Skip to content

Commit 7565ab6

Browse files
committed
fixed curl command arg parsing issue
1 parent 7aacdd7 commit 7565ab6

File tree

2 files changed

+221
-70
lines changed

2 files changed

+221
-70
lines changed

common/commands/curl.go

Lines changed: 118 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,15 @@ func (curlCmd *CurlCommand) Run() error {
6363
return errorutils.CheckErrorf("Curl command must not include certificate flag (--cert or --key).")
6464
}
6565

66-
// Build the full URL from the API path (first non-flag argument).
67-
// Remove the API path and append the full URL at the end.
68-
if err := curlCmd.buildAndAppendUrl(); err != nil {
66+
// Get target url for the curl command.
67+
uriIndex, targetUri, err := curlCmd.buildCommandUrl(curlCmd.url)
68+
if err != nil {
6969
return err
7070
}
7171

72+
// Replace url argument with complete url.
73+
curlCmd.arguments[uriIndex] = targetUri
74+
7275
cmdWithoutCreds := strings.Join(curlCmd.arguments, " ")
7376
// Add credentials to curl command.
7477
credentialsMessage := curlCmd.addCommandCredentials()
@@ -103,81 +106,28 @@ func (curlCmd *CurlCommand) addCommandCredentials() string {
103106
return certificateHelpPrefix + "-u***:***"
104107
}
105108

106-
// buildAndAppendUrl finds the first non-flag argument (the API path), removes it,
107-
// builds the full URL, and appends it at the end. This allows curl flags to appear in any order.
108-
func (curlCmd *CurlCommand) buildAndAppendUrl() error {
109-
// Common curl flags that take a value in the next argument
110-
flagsWithValues := map[string]bool{
111-
"-X": true, "-H": true, "-d": true, "-o": true, "-A": true, "-e": true,
112-
"-T": true, "-b": true, "-c": true, "-F": true, "-m": true, "-w": true,
113-
"-x": true, "-y": true, "-z": true, "-C": true, "-K": true, "-E": true,
114-
"--request": true, "--header": true, "--data": true, "--output": true,
115-
"--user-agent": true, "--referer": true, "--upload-file": true,
116-
"--cookie": true, "--cookie-jar": true, "--form": true, "--max-time": true,
117-
"--write-out": true, "--proxy": true, "--cert": true, "--key": true,
118-
"--cacert": true, "--capath": true, "--connect-timeout": true,
119-
"--retry": true, "--retry-delay": true, "--retry-max-time": true,
120-
"--speed-limit": true, "--speed-time": true, "--limit-rate": true,
121-
"--max-filesize": true, "--max-redirs": true, "--data-binary": true,
122-
"--data-urlencode": true, "--data-raw": true, "--data-ascii": true,
109+
func (curlCmd *CurlCommand) buildCommandUrl(url string) (uriIndex int, uriValue string, err error) {
110+
// Find command's URL argument.
111+
// Representing the target API for the Curl command.
112+
uriIndex, uriValue = curlCmd.findUriValueAndIndex()
113+
if uriIndex == -1 {
114+
err = errorutils.CheckErrorf("Could not find argument in curl command.")
115+
return
123116
}
124117

125-
// Find the first non-flag argument (the API path)
126-
// Skip arguments that are values for flags
127-
apiPathIndex := -1
128-
skipNext := false
129-
130-
for i, arg := range curlCmd.arguments {
131-
// Skip if this is a flag value
132-
if skipNext {
133-
skipNext = false
134-
continue
135-
}
136-
137-
// Check if this is a flag
138-
if strings.HasPrefix(arg, "-") {
139-
// Check if it's a flag that takes a value (and value is not inline)
140-
if flagsWithValues[arg] {
141-
skipNext = true
142-
}
143-
// Check for long flags with inline values like --header=value
144-
if strings.Contains(arg, "=") {
145-
skipNext = false
146-
}
147-
// For short flags, check if value is inline like -XGET
148-
if len(arg) > 2 && !strings.HasPrefix(arg, "--") {
149-
skipNext = false
150-
}
151-
continue
152-
}
153-
154-
// Found a non-flag argument that's not a flag value - this is the API path
155-
apiPathIndex = i
156-
break
157-
}
158-
159-
if apiPathIndex == -1 {
160-
return errorutils.CheckErrorf("Could not find API path argument in curl command.")
161-
}
162-
163-
apiPath := curlCmd.arguments[apiPathIndex]
164-
165118
// If user provided full-url, throw an error.
166-
if strings.HasPrefix(apiPath, "http://") || strings.HasPrefix(apiPath, "https://") {
167-
return errorutils.CheckErrorf("Curl command must not include full-url, but only the REST API URI (e.g '/api/system/ping').")
119+
if strings.HasPrefix(uriValue, "http://") || strings.HasPrefix(uriValue, "https://") {
120+
err = errorutils.CheckErrorf("Curl command must not include full-url, but only the REST API URI (e.g '/api/system/ping').")
121+
return
168122
}
169123

170-
// Remove the API path from its current position
171-
curlCmd.arguments = append(curlCmd.arguments[:apiPathIndex], curlCmd.arguments[apiPathIndex+1:]...)
172-
173124
// Trim '/' prefix if exists.
174-
apiPath = strings.TrimPrefix(apiPath, "/")
125+
uriValue = strings.TrimPrefix(uriValue, "/")
175126

176-
// Build full URL and append at the end
177-
fullUrl := curlCmd.url + apiPath
178-
curlCmd.arguments = append(curlCmd.arguments, fullUrl)
127+
// Attach url to the api.
128+
uriValue = url + uriValue
179129

180-
return nil
130+
return
181131
}
182132

183133
// Returns server details
@@ -191,6 +141,104 @@ func (curlCmd *CurlCommand) GetServerDetails() (*config.ServerDetails, error) {
191141
return config.GetSpecificConfig(serverIdValue, true, true)
192142
}
193143

144+
// curlBooleanFlags contains curl flags that do NOT take a value.
145+
var curlBooleanFlags = map[string]bool{
146+
"-#": true, "-0": true, "-1": true, "-2": true, "-3": true, "-4": true, "-6": true,
147+
"-a": true, "-B": true, "-f": true, "-g": true, "-G": true, "-I": true, "-i": true,
148+
"-j": true, "-J": true, "-k": true, "-l": true, "-L": true, "-M": true, "-n": true,
149+
"-N": true, "-O": true, "-p": true, "-q": true, "-R": true, "-s": true, "-S": true,
150+
"-v": true, "-V": true, "-Z": true,
151+
"--anyauth": true, "--append": true, "--basic": true, "--ca-native": true,
152+
"--cert-status": true, "--compressed": true, "--compressed-ssh": true,
153+
"--create-dirs": true, "--crlf": true, "--digest": true, "--disable": true,
154+
"--disable-eprt": true, "--disable-epsv": true, "--disallow-username-in-url": true,
155+
"--doh-cert-status": true, "--doh-insecure": true, "--fail": true,
156+
"--fail-early": true, "--fail-with-body": true, "--false-start": true,
157+
"--form-escape": true, "--ftp-create-dirs": true, "--ftp-pasv": true,
158+
"--ftp-pret": true, "--ftp-skip-pasv-ip": true, "--ftp-ssl-ccc": true,
159+
"--ftp-ssl-control": true, "--get": true, "--globoff": true,
160+
"--haproxy-protocol": true, "--head": true, "--http0.9": true, "--http1.0": true,
161+
"--http1.1": true, "--http2": true, "--http2-prior-knowledge": true,
162+
"--http3": true, "--http3-only": true, "--ignore-content-length": true,
163+
"--include": true, "--insecure": true, "--ipv4": true, "--ipv6": true,
164+
"--junk-session-cookies": true, "--list-only": true, "--location": true,
165+
"--location-trusted": true, "--mail-rcpt-allowfails": true, "--manual": true,
166+
"--metalink": true, "--negotiate": true, "--netrc": true, "--netrc-optional": true,
167+
"--next": true, "--no-alpn": true, "--no-buffer": true, "--no-clobber": true,
168+
"--no-keepalive": true, "--no-npn": true, "--no-progress-meter": true,
169+
"--no-sessionid": true, "--ntlm": true, "--ntlm-wb": true, "--parallel": true,
170+
"--parallel-immediate": true, "--path-as-is": true, "--post301": true,
171+
"--post302": true, "--post303": true, "--progress-bar": true,
172+
"--proxy-anyauth": true, "--proxy-basic": true, "--proxy-ca-native": true,
173+
"--proxy-digest": true, "--proxy-http2": true, "--proxy-insecure": true,
174+
"--proxy-negotiate": true, "--proxy-ntlm": true, "--proxy-ssl-allow-beast": true,
175+
"--proxy-ssl-auto-client-cert": true, "--proxy-tlsv1": true, "--proxytunnel": true,
176+
"--raw": true, "--remote-header-name": true, "--remote-name": true,
177+
"--remote-name-all": true, "--remote-time": true, "--remove-on-error": true,
178+
"--retry-all-errors": true, "--retry-connrefused": true, "--sasl-ir": true,
179+
"--show-error": true, "--silent": true, "--socks5-basic": true,
180+
"--socks5-gssapi": true, "--socks5-gssapi-nec": true, "--ssl": true,
181+
"--ssl-allow-beast": true, "--ssl-auto-client-cert": true, "--ssl-no-revoke": true,
182+
"--ssl-reqd": true, "--ssl-revoke-best-effort": true, "--sslv2": true,
183+
"--sslv3": true, "--styled-output": true, "--suppress-connect-headers": true,
184+
"--tcp-fastopen": true, "--tcp-nodelay": true, "--tftp-no-options": true,
185+
"--tlsv1": true, "--tlsv1.0": true, "--tlsv1.1": true, "--tlsv1.2": true,
186+
"--tlsv1.3": true, "--tr-encoding": true, "--trace-ids": true,
187+
"--trace-time": true, "--use-ascii": true, "--verbose": true, "--version": true,
188+
"--xattr": true,
189+
}
190+
191+
// Find the URL argument in the Curl Command.
192+
// A command flag is prefixed by '-' or '--'.
193+
// Use this method ONLY after removing all JFrog-CLI flags, i.e. flags in the form: '--my-flag=value' are not allowed.
194+
// An argument is any provided candidate which is not a flag or a flag value.
195+
func (curlCmd *CurlCommand) findUriValueAndIndex() (int, string) {
196+
skipNextArg := false
197+
for index, arg := range curlCmd.arguments {
198+
// Check if this arg should be skipped (it's a value for the previous flag)
199+
if skipNextArg {
200+
skipNextArg = false
201+
continue
202+
}
203+
204+
// Check if this is a flag
205+
if strings.HasPrefix(arg, "-") {
206+
// Check for flags with inline values like --header=value or -XGET
207+
if strings.HasPrefix(arg, "--") && strings.Contains(arg, "=") {
208+
continue
209+
}
210+
211+
// Check if this is a known standalone flag (no value needed)
212+
if curlBooleanFlags[arg] {
213+
continue
214+
}
215+
216+
// For short flags (not starting with --)
217+
if !strings.HasPrefix(arg, "--") && len(arg) > 2 {
218+
// Could be inline value (e.g., -XGET, -ofile.txt) or combined flags (e.g., -vvv, -sS)
219+
// Check if the base flag is standalone
220+
baseFlag := arg[:2]
221+
if curlBooleanFlags[baseFlag] {
222+
// Combined standalone flags like -vvv or -sS, no skip needed
223+
continue
224+
}
225+
// Inline value like -XGET or -ofile.txt, no skip needed
226+
continue
227+
}
228+
229+
// Flag not in standalone list - it takes a value in the next argument
230+
skipNextArg = true
231+
continue
232+
}
233+
234+
// Found a non-flag argument - this is the URL/API path
235+
return index, arg
236+
}
237+
238+
// If reached here, didn't find an argument.
239+
return -1, ""
240+
}
241+
194242
// Return true if the curl command includes credentials flag.
195243
// The searched flags are not CLI flags.
196244
func (curlCmd *CurlCommand) isCredentialsFlagExists() bool {

common/commands/curl_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,106 @@ func TestBuildCommandUrl(t *testing.T) {
102102
})
103103
}
104104
}
105+
106+
func TestFindUriWithStandaloneFlags(t *testing.T) {
107+
tests := []struct {
108+
name string
109+
arguments []string
110+
expectedUriIndex int
111+
expectedUri string
112+
}{
113+
{
114+
name: "regression_silent_show_error_verbose",
115+
arguments: []string{"-s", "--show-error", "api/repositories/dev-master-maven-local", "--verbose"},
116+
expectedUriIndex: 2,
117+
expectedUri: "api/repositories/dev-master-maven-local",
118+
},
119+
{
120+
name: "bug_case_1_output_location_verbose",
121+
arguments: []string{"-o", "helm.tar.gz", "-L", "-vvv", "helm-sh/helm-v3.19.0-linux-amd64.tar.gz"},
122+
expectedUriIndex: 4,
123+
expectedUri: "helm-sh/helm-v3.19.0-linux-amd64.tar.gz",
124+
},
125+
{
126+
name: "bug_case_2_output_verbose_location",
127+
arguments: []string{"-o", "helm.tar.gz", "-vvv", "-L", "helm-sh/helm-v3.19.0-linux-amd64.tar.gz"},
128+
expectedUriIndex: 4,
129+
expectedUri: "helm-sh/helm-v3.19.0-linux-amd64.tar.gz",
130+
},
131+
{
132+
name: "bug_case_3_output_location_silent",
133+
arguments: []string{"-o", "helm.tar.gz", "-L", "-s", "helm-sh/helm-v3.19.0-linux-amd64.tar.gz"},
134+
expectedUriIndex: 4,
135+
expectedUri: "helm-sh/helm-v3.19.0-linux-amd64.tar.gz",
136+
},
137+
{
138+
name: "bug_case_4_location_output",
139+
arguments: []string{"-L", "-o", "helm.tar.gz", "helm-sh/helm-v3.19.0-linux-amd64.tar.gz"},
140+
expectedUriIndex: 3,
141+
expectedUri: "helm-sh/helm-v3.19.0-linux-amd64.tar.gz",
142+
},
143+
{
144+
name: "bug_case_5_output_location",
145+
arguments: []string{"-o", "helm.tar.gz", "-L", "helm-sh/helm-v3.19.0-linux-amd64.tar.gz"},
146+
expectedUriIndex: 3,
147+
expectedUri: "helm-sh/helm-v3.19.0-linux-amd64.tar.gz",
148+
},
149+
{
150+
name: "bug_case_6_location_verbose_output",
151+
arguments: []string{"-L", "-vvv", "-o", "helm.tar.gz", "helm-sh/helm-v3.19.0-linux-amd64.tar.gz"},
152+
expectedUriIndex: 4,
153+
expectedUri: "helm-sh/helm-v3.19.0-linux-amd64.tar.gz",
154+
},
155+
{
156+
name: "bug_case_7_location_output_verbose",
157+
arguments: []string{"-L", "-o", "helm.tar.gz", "-vvv", "helm-sh/helm-v3.19.0-linux-amd64.tar.gz"},
158+
expectedUriIndex: 4,
159+
expectedUri: "helm-sh/helm-v3.19.0-linux-amd64.tar.gz",
160+
},
161+
{
162+
name: "multiple_standalone_flags_combined",
163+
arguments: []string{"-sS", "-L", "api/system/ping"},
164+
expectedUriIndex: 2,
165+
expectedUri: "api/system/ping",
166+
},
167+
{
168+
name: "long_standalone_flags",
169+
arguments: []string{"--silent", "--show-error", "--location", "api/system/ping"},
170+
expectedUriIndex: 3,
171+
expectedUri: "api/system/ping",
172+
},
173+
{
174+
name: "mixed_short_long_standalone",
175+
arguments: []string{"-X", "GET", "-H", "Content-Type: application/json", "--verbose", "--insecure", "api/repositories"},
176+
expectedUriIndex: 6,
177+
expectedUri: "api/repositories",
178+
},
179+
{
180+
name: "inline_short_flag_value",
181+
arguments: []string{"-XPOST", "-HContent-Type:application/json", "-L", "api/repositories"},
182+
expectedUriIndex: 3,
183+
expectedUri: "api/repositories",
184+
},
185+
{
186+
name: "long_flag_with_equals",
187+
arguments: []string{"--request=GET", "--header=Accept:application/json", "-v", "api/system/ping"},
188+
expectedUriIndex: 3,
189+
expectedUri: "api/system/ping",
190+
},
191+
}
192+
193+
command := &CurlCommand{}
194+
for _, test := range tests {
195+
t.Run(test.name, func(t *testing.T) {
196+
command.arguments = test.arguments
197+
actualIndex, actualUri := command.findUriValueAndIndex()
198+
199+
if actualIndex != test.expectedUriIndex {
200+
t.Errorf("Expected URI index: %d, got: %d. Arguments: %v", test.expectedUriIndex, actualIndex, test.arguments)
201+
}
202+
if actualUri != test.expectedUri {
203+
t.Errorf("Expected URI: %s, got: %s. Arguments: %v", test.expectedUri, actualUri, test.arguments)
204+
}
205+
})
206+
}
207+
}

0 commit comments

Comments
 (0)