github.com/henvic/wedeploycli@v1.7.6-0.20200319005353-3630f582f284/command/curl/curl.go (about) 1 package curl 2 3 // NOTE: curl's --url is not used due to conflicting lcp --url flag 4 // However, this code considers it (though the code wounever be executed), 5 // for the sake of completion [and if things change]. 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 "net/url" 13 "os" 14 "os/exec" 15 "strings" 16 17 "github.com/hashicorp/errwrap" 18 "github.com/henvic/wedeploycli/cmdflagsfromhost" 19 "github.com/henvic/wedeploycli/command/curl/internal/curlargs" 20 "github.com/henvic/wedeploycli/command/internal/we" 21 "github.com/henvic/wedeploycli/config" 22 "github.com/henvic/wedeploycli/prettyjson" 23 "github.com/henvic/wedeploycli/verbose" 24 "github.com/spf13/cobra" 25 "github.com/spf13/pflag" 26 ) 27 28 // CurlCmd do curl requests using the user credential 29 var CurlCmd = &cobra.Command{ 30 Use: "curl", 31 Short: "Do requests with curl", 32 Long: `Do requests with curl 33 Requests are piped to curl with credentials attached and paths expanded. 34 Pattern: lcp curl [curl options...] <url> 35 Use "curl --help" to see curl usage options. 36 `, 37 Example: ` lcp curl /projects 38 lcp curl /plans/user 39 lcp curl https://api.liferay.cloud/projects`, 40 // maybe --pretty=false to disable pipe, should add example 41 RunE: (&curlRunner{}).run, 42 Hidden: true, 43 DisableFlagParsing: true, 44 } 45 46 // EnableCmd for enabling using "lcp curl" 47 var EnableCmd = &cobra.Command{ 48 Use: "enable", 49 Short: "Enable curl commands", 50 RunE: enableRun, 51 Args: cobra.NoArgs, 52 } 53 54 // DisableCmd for disabling using "lcp curl" 55 var DisableCmd = &cobra.Command{ 56 Use: "disable", 57 Short: "Disable curl commands", 58 RunE: disableRun, 59 Args: cobra.NoArgs, 60 } 61 62 var setupHost = cmdflagsfromhost.SetupHost{ 63 Pattern: cmdflagsfromhost.RemotePattern, 64 } 65 66 var print bool 67 var noPretty bool 68 69 func init() { 70 CurlCmd.AddCommand(EnableCmd) 71 CurlCmd.AddCommand(DisableCmd) 72 73 CurlCmd.Flags().BoolVar( 74 &print, 75 "print", 76 false, 77 "Print command instead of invoking") 78 CurlCmd.Flags().BoolVar( 79 &noPretty, 80 "no-pretty", 81 false, 82 "Don't pretty print JSON") 83 setupHost.Init(CurlCmd) 84 } 85 86 type argsF struct { 87 input []string 88 pos int 89 90 weArgs []string 91 curlArgs []string 92 93 pfs []*pflag.Flag 94 } 95 96 func (af *argsF) maybeGetBoolArgument() (is bool) { 97 arg := af.input[af.pos] 98 99 // -H might be either --long-help or curl header 100 if arg == "-H" && (af.pos+1 >= len(af.input) || 101 strings.HasPrefix(af.input[af.pos+1], "-")) { 102 af.weArgs = append(af.weArgs, arg) 103 return true 104 } 105 106 for _, p := range af.pfs { 107 if arg == "--"+p.Name || arg == "-"+p.Shorthand { 108 // -H requires a special treatment, given above 109 if arg == "-H" { 110 continue 111 } 112 113 af.weArgs = append(af.weArgs, arg) 114 115 if p.Value.Type() != "bool" && af.pos+1 < len(af.input) { 116 af.weArgs = append(af.weArgs, af.input[af.pos+1]) 117 af.pos++ 118 } 119 120 return true 121 } 122 } 123 124 return false 125 } 126 127 func (cr *curlRunner) parseArguments() (weArgs, curlArgs []string) { 128 // ignore "lcp curl" 129 var commandLength = len(strings.Split(cr.cmd.CommandPath(), " ")) 130 131 if len(os.Args) <= commandLength { 132 return []string{}, []string{} 133 } 134 135 var af = argsF{ 136 input: os.Args[commandLength:], 137 } 138 139 cr.cmd.Flags().VisitAll(func(f *pflag.Flag) { 140 af.pfs = append(af.pfs, f) 141 }) 142 143 for { 144 if af.pos >= len(af.input) { 145 break 146 } 147 148 if got := af.maybeGetBoolArgument(); !got { 149 af.curlArgs = append(af.curlArgs, af.input[af.pos]) 150 } 151 152 af.pos++ 153 } 154 155 return af.weArgs, af.curlArgs 156 } 157 158 func isSafeInfrastructureURL(wectx config.Context, param string) bool { 159 u, err := url.Parse(param) 160 extractedHost := fmt.Sprintf("%s://%s", u.Scheme, u.Host) 161 162 if err != nil || extractedHost == wectx.Infrastructure() { 163 return true 164 } 165 166 return false 167 } 168 169 // UnsafeURLError is used when a URL is dangerous to add on the curl command 170 type UnsafeURLError struct { 171 url string 172 } 173 174 func (u UnsafeURLError) Error() string { 175 return fmt.Sprintf("refusing due to possibly unsafe URL value: %v", u.url) 176 } 177 178 func extractRemoteFromFullPath(wectx config.Context, fullPath string) (string, error) { 179 var conf = wectx.Config() 180 u, err := url.Parse(fullPath) 181 182 if err != nil { 183 return "", err 184 } 185 186 var params = conf.GetParams() 187 var rl = params.Remotes 188 189 for _, key := range rl.Keys() { 190 r := rl.Get(key) 191 i, err := url.Parse(r.InfrastructureServer()) 192 193 if err != nil { 194 continue 195 } 196 197 if u.Host == i.Host && u.Scheme == i.Scheme { 198 return key, err 199 } 200 } 201 202 return "", nil 203 } 204 205 func extractAlternativeRemote(wectx config.Context, params []string) (string, error) { 206 var alternative string 207 208 for i, p := range params { 209 if strings.HasPrefix(p, "-") || strings.HasPrefix(p, "/") { 210 continue 211 } 212 213 if i == 0 { 214 var remote, err = extractRemoteFromFullPath(wectx, p) 215 216 if err != nil { 217 continue 218 } 219 220 if alternative != "" && alternative != remote { 221 return "", UnsafeURLError{p} 222 } 223 224 alternative = remote 225 continue 226 } 227 228 var iss = curlargs.IsCLIStringArgument(params[i-1]) 229 230 if !iss || (iss && params[i-1] == "--url") { 231 var remote, err = extractRemoteFromFullPath(wectx, p) 232 233 if err != nil { 234 continue 235 } 236 237 if alternative != "" && alternative != remote { 238 return "", UnsafeURLError{p} 239 } 240 241 alternative = remote 242 continue 243 } 244 } 245 246 // at the end, check if it's not the same and ignore if it is 247 if alternative == wectx.Remote() { 248 alternative = "" 249 } 250 251 return alternative, nil 252 } 253 254 func expandPathsToFullRequests(wectx config.Context, params []string) ([]string, error) { 255 var out []string 256 257 for i, p := range params { 258 if strings.HasPrefix(p, "-") { 259 out = append(out, p) 260 continue 261 } 262 263 // let's try to expand URLs (e.g., "lcp curl /projects" should work) 264 if strings.HasPrefix(p, "/") { 265 if i == 0 { 266 out = append(out, fmt.Sprintf("%v%v", wectx.Infrastructure(), p)) 267 continue 268 } 269 270 // expand string arguments, except for --url 271 if curlargs.IsCLIStringArgument(params[i-1]) && params[i-1] != "--url" { 272 out = append(out, p) 273 continue 274 } 275 276 out = append(out, fmt.Sprintf("%v%v", wectx.Infrastructure(), p)) 277 continue 278 } 279 280 if i == 0 { 281 if !isSafeInfrastructureURL(wectx, p) { 282 return nil, UnsafeURLError{p} 283 } 284 285 out = append(out, p) 286 continue 287 } 288 289 // expand string arguments, except for --url 290 if curlargs.IsCLIStringArgument(params[i-1]) { 291 if params[i-1] == "--url" && !isSafeInfrastructureURL(wectx, p) { 292 return nil, UnsafeURLError{p} 293 } 294 295 out = append(out, p) 296 continue 297 } 298 299 if !isSafeInfrastructureURL(wectx, p) { 300 return nil, UnsafeURLError{p} 301 } 302 303 out = append(out, p) 304 } 305 306 return out, nil 307 } 308 309 func enableRun(cmd *cobra.Command, args []string) (err error) { 310 var wectx = we.Context() 311 var conf = wectx.Config() 312 var params = conf.GetParams() 313 314 params.EnableCURL = true 315 316 conf.SetParams(params) 317 318 return conf.Save() 319 } 320 321 func disableRun(cmd *cobra.Command, args []string) (err error) { 322 var wectx = we.Context() 323 var conf = wectx.Config() 324 var params = conf.GetParams() 325 326 params.EnableCURL = false 327 328 conf.SetParams(params) 329 330 return conf.Save() 331 } 332 333 func maybeChangeRemote(wectx config.Context, cmd *cobra.Command, weArgs []string, alternative string) error { 334 // if no change on remote, shortcuit it 335 if alternative == "" { 336 return nil 337 } 338 339 if alternative != "" && (cmd.Flag("remote").Changed || cmd.Flag("url").Changed) { 340 return fmt.Errorf(`ambiguous remote options: "%s" and "%s"`, 341 setupHost.Remote(), alternative) 342 } 343 344 if err := cmd.Flag("remote").Value.Set(alternative); err != nil { 345 return errwrap.Wrapf("can't override remote value: {{err}}", err) 346 } 347 348 return wectx.SetEndpoint(setupHost.Remote()) 349 } 350 351 type curlRunner struct { 352 cmd *cobra.Command 353 } 354 355 func (cr *curlRunner) run(cmd *cobra.Command, args []string) error { 356 cr.cmd = cmd 357 358 // Let's try to avoid verbose messages from the CLI 359 // get in the way of verbose of the curl command 360 defer func() { 361 verbose.Enabled = false 362 }() 363 364 var wectx = we.Context() 365 var conf = wectx.Config() 366 var params = conf.GetParams() 367 368 var weArgs, curlArgs = cr.parseArguments() 369 370 cmd.DisableFlagParsing = false 371 if err := cmd.ParseFlags(weArgs); err != nil { 372 return err 373 } 374 375 if cmd.Flag("help").Value.String() == "true" || 376 cmd.Flag("long-help").Value.String() == "true" || len(args) == 0 { 377 return cmd.Help() 378 } 379 380 if !params.EnableCURL { 381 _, _ = fmt.Fprintln(os.Stderr, 382 `This command is not enabled by default as it might be dangerous for security. 383 Using it might make you inadvertently expose private data. Continue at your own risk.`) 384 385 return fmt.Errorf(`you must enable this command first with "%v"`, 386 EnableCmd.CommandPath()) 387 } 388 389 if err := setupHost.Process(context.Background(), wectx); err != nil { 390 return err 391 } 392 393 alternative, err := extractAlternativeRemote(wectx, curlArgs) 394 395 if err != nil { 396 return err 397 } 398 399 if err = maybeChangeRemote(wectx, cmd, weArgs, alternative); err != nil { 400 return err 401 } 402 403 if verbose.Enabled { 404 curlArgs = append(curlArgs, "--verbose") 405 } 406 407 curlArgs, err = expandPathsToFullRequests(wectx, curlArgs) 408 409 if err != nil { 410 return err 411 } 412 413 token := wectx.Token() 414 415 if token != "" { 416 curlArgs = append(curlArgs, "-H") 417 curlArgs = append(curlArgs, fmt.Sprintf("Authorization: Bearer %s", token)) 418 } 419 420 curlArgs = append([]string{"-sS"}, curlArgs...) 421 422 if print { 423 printCURLCommand(curlArgs) 424 return nil 425 } 426 427 if !noPretty { 428 return curlPretty(context.Background(), curlArgs) 429 } 430 431 return curl(context.Background(), curlArgs) 432 } 433 434 func curl(ctx context.Context, params []string) error { 435 verbose.Debug(fmt.Sprintf("Running curl %v", strings.Join(params, " "))) 436 437 var cmd = exec.CommandContext(ctx, "curl", params...) // #nosec 438 cmd.Stdin = os.Stdin 439 cmd.Stderr = os.Stderr 440 cmd.Stdout = os.Stdout 441 return cmd.Run() 442 } 443 444 func curlPretty(ctx context.Context, params []string) error { 445 verbose.Debug(fmt.Sprintf("Running curl %v", strings.Join(params, " "))) 446 447 var ( 448 buf bytes.Buffer 449 bufErr bytes.Buffer 450 ) 451 452 var cmd = exec.CommandContext(ctx, "curl", params...) // #nosec 453 cmd.Stdin = os.Stdin 454 455 cmd.Stderr = io.MultiWriter(&bufErr, os.Stderr) 456 cmd.Stdout = &buf 457 458 if err := cmd.Run(); err != nil { 459 return err 460 } 461 462 maybePrettyPrintJSON(bufErr.Bytes(), buf.Bytes()) 463 return nil 464 } 465 466 func maybePrettyPrintJSON(headersOrErr, body []byte) { 467 if verbose.Enabled && 468 !bytes.Contains(bytes.ToLower(headersOrErr), []byte("\n< content-type: application/json")) { 469 fmt.Println(string(body)) 470 return 471 } 472 473 fmt.Print(string(prettyjson.Pretty(body))) 474 } 475 476 func printCURLCommand(args []string) { 477 fmt.Printf("curl") 478 479 for _, a := range args { 480 if strings.ContainsRune(a, ' ') { 481 fmt.Printf(` "%s"`, a) 482 break 483 } 484 485 fmt.Printf(" %s", a) 486 } 487 488 fmt.Printf("\n") 489 }