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  }