github.com/keybase/client/go@v0.0.0-20240520164431-4f512a4c85a3/client/cmd_apicall.go (about)

     1  // Copyright 2016 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package client
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"net/url"
    10  	"strings"
    11  
    12  	"github.com/keybase/cli"
    13  	"github.com/keybase/client/go/libcmdline"
    14  	"github.com/keybase/client/go/libkb"
    15  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    16  	"golang.org/x/net/context"
    17  )
    18  
    19  type httpMethod int
    20  
    21  const (
    22  	GET httpMethod = iota
    23  	POST
    24  	DELETE
    25  )
    26  
    27  func (m httpMethod) String() string {
    28  	switch m {
    29  	case GET:
    30  		return "GET"
    31  	case POST:
    32  		return "POST"
    33  	case DELETE:
    34  		return "DELETE"
    35  	}
    36  	return "<unknown>"
    37  }
    38  
    39  type CmdAPICall struct {
    40  	endpoint     string
    41  	method       httpMethod
    42  	args         []keybase1.StringKVPair
    43  	httpStatuses []int
    44  	appStatuses  []int
    45  	JSONPayload  []keybase1.StringKVPair
    46  
    47  	parsedHost string
    48  	text       bool
    49  
    50  	libkb.Contextified
    51  }
    52  
    53  func NewCmdAPICall(cl *libcmdline.CommandLine, g *libkb.GlobalContext) cli.Command {
    54  	return cli.Command{
    55  		Name: "apicall",
    56  		// No "Usage" field makes it hidden in command list.
    57  		ArgumentHelp: "<endpoint>",
    58  		Description:  "Send a request to the API Server",
    59  		Action: func(c *cli.Context) {
    60  			cl.ChooseCommand(&CmdAPICall{
    61  				Contextified: libkb.NewContextified(g),
    62  			}, "apicall", c)
    63  		},
    64  		Flags: []cli.Flag{
    65  			cli.StringFlag{
    66  				Name:  "m, method",
    67  				Usage: "Specify the HTTP method for the request",
    68  			},
    69  			cli.StringSliceFlag{
    70  				Name:  "a, arg",
    71  				Usage: "Specify an argument in the form name=value",
    72  				Value: &cli.StringSlice{},
    73  			},
    74  			cli.StringFlag{
    75  				Name:  "json-payload",
    76  				Usage: "Specify the JSON payload for the POST request",
    77  			},
    78  			cli.IntSliceFlag{
    79  				Name:  "s, status",
    80  				Usage: "Specify an acceptable HTTP status code",
    81  				Value: &cli.IntSlice{},
    82  			},
    83  			cli.IntSliceFlag{
    84  				Name:  "p, appstatus",
    85  				Usage: "Specify an acceptable app status code",
    86  				Value: &cli.IntSlice{},
    87  			},
    88  			cli.BoolFlag{
    89  				Name:  "url",
    90  				Usage: "Pass full keybase.io URL with query parameters instead of an endpoint.",
    91  			},
    92  			cli.BoolFlag{
    93  				Name:  "text",
    94  				Usage: "endpoint is text instead of json.",
    95  			},
    96  		},
    97  	}
    98  }
    99  
   100  func (c *CmdAPICall) Run() error {
   101  	if c.parsedHost != "" {
   102  		serverURI, err := c.G().Env.GetServerURI()
   103  		if err != nil {
   104  			return err
   105  		}
   106  
   107  		if !strings.EqualFold(c.parsedHost, serverURI) {
   108  			return fmt.Errorf("Unexpected host in URL mode: %s. This only works for Keybase API.", c.parsedHost)
   109  		}
   110  		c.G().Log.Info("Parsed URL as endpoint: %q, args: %+v", c.endpoint, c.args)
   111  	}
   112  
   113  	dui := c.G().UI.GetDumbOutputUI()
   114  	cli, err := GetAPIServerClient(c.G())
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	var res keybase1.APIRes
   120  	switch c.method {
   121  	case GET:
   122  		arg := c.formGetArg()
   123  		res, err = cli.GetWithSession(context.TODO(), arg)
   124  		if err != nil {
   125  			return err
   126  		}
   127  	case POST:
   128  		if c.JSONPayload != nil {
   129  			arg := c.formPostJSONArg()
   130  			res, err = cli.PostJSON(context.TODO(), arg)
   131  		} else {
   132  			arg := c.formPostArg()
   133  			res, err = cli.Post(context.TODO(), arg)
   134  		}
   135  
   136  		if err != nil {
   137  			return err
   138  		}
   139  	case DELETE:
   140  		arg := c.formDeleteArg()
   141  		res, err = cli.Delete(context.TODO(), arg)
   142  		if err != nil {
   143  			return err
   144  		}
   145  	}
   146  
   147  	dui.Printf("%s", res.Body)
   148  	return nil
   149  }
   150  
   151  func (c *CmdAPICall) formGetArg() (res keybase1.GetWithSessionArg) {
   152  	res.Endpoint = c.endpoint
   153  	res.Args = c.args
   154  	res.HttpStatus = c.httpStatuses
   155  	res.AppStatusCode = c.appStatuses
   156  	res.UseText = &c.text
   157  	return
   158  }
   159  
   160  func (c *CmdAPICall) formDeleteArg() (res keybase1.DeleteArg) {
   161  	res.Endpoint = c.endpoint
   162  	res.Args = c.args
   163  	res.HttpStatus = c.httpStatuses
   164  	res.AppStatusCode = c.appStatuses
   165  	return
   166  }
   167  
   168  func (c *CmdAPICall) formPostArg() (res keybase1.PostArg) {
   169  	res.Endpoint = c.endpoint
   170  	res.Args = c.args
   171  	res.HttpStatus = c.httpStatuses
   172  	res.AppStatusCode = c.appStatuses
   173  	return
   174  }
   175  
   176  func (c *CmdAPICall) formPostJSONArg() (res keybase1.PostJSONArg) {
   177  	res.Endpoint = c.endpoint
   178  	res.Args = c.args
   179  	res.HttpStatus = c.httpStatuses
   180  	res.AppStatusCode = c.appStatuses
   181  	res.JSONPayload = c.JSONPayload
   182  	return
   183  }
   184  
   185  func (c *CmdAPICall) validateMethod(m string) (httpMethod, error) {
   186  	if m == "" {
   187  		return GET, nil
   188  	} else if strings.ToLower(m) == "post" {
   189  		return POST, nil
   190  	} else if strings.ToLower(m) == "get" {
   191  		return GET, nil
   192  	} else if strings.ToLower(m) == "delete" {
   193  		return DELETE, nil
   194  	}
   195  	return 0, fmt.Errorf("invalid method specified: %s", m)
   196  }
   197  
   198  func (c *CmdAPICall) parseArgument(a string) (res keybase1.StringKVPair, err error) {
   199  	toks := strings.Split(a, "=")
   200  	if len(toks) != 2 {
   201  		err = fmt.Errorf("invalid argument: %s", a)
   202  		return
   203  	}
   204  	return keybase1.StringKVPair{Key: toks[0], Value: toks[1]}, nil
   205  }
   206  
   207  func (c *CmdAPICall) addArgument(arg keybase1.StringKVPair) {
   208  	c.args = append(c.args, arg)
   209  }
   210  
   211  type JSONInput map[string]json.RawMessage
   212  
   213  func (c *CmdAPICall) parseJSONPayload(p string) ([]keybase1.StringKVPair, error) {
   214  	var input JSONInput
   215  	err := json.Unmarshal([]byte(p), &input)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	var res []keybase1.StringKVPair
   221  	for k, v := range input {
   222  		res = append(res, keybase1.StringKVPair{Key: k, Value: string(v[:])})
   223  	}
   224  
   225  	return res, nil
   226  }
   227  
   228  func (c *CmdAPICall) ParseArgv(ctx *cli.Context) error {
   229  	var err error
   230  	nargs := len(ctx.Args())
   231  	if nargs == 0 {
   232  		return fmt.Errorf("endpoint is required")
   233  	} else if nargs != 1 {
   234  		return fmt.Errorf("expected 1 argument (endpoint), got %d: %v", nargs, ctx.Args())
   235  	}
   236  
   237  	c.endpoint = ctx.Args()[0]
   238  
   239  	if c.method, err = c.validateMethod(ctx.String("method")); err != nil {
   240  		return err
   241  	}
   242  
   243  	args := ctx.StringSlice("arg")
   244  	for _, a := range args {
   245  		pa, err := c.parseArgument(a)
   246  		if err != nil {
   247  			return err
   248  		}
   249  		c.addArgument(pa)
   250  	}
   251  
   252  	httpStatuses := ctx.IntSlice("status")
   253  	c.httpStatuses = append(c.httpStatuses, httpStatuses...)
   254  
   255  	appStatuses := ctx.IntSlice("appstatus")
   256  	c.appStatuses = append(c.appStatuses, appStatuses...)
   257  
   258  	payload := ctx.String("json-payload")
   259  	if payload != "" {
   260  		if c.JSONPayload, err = c.parseJSONPayload(payload); err != nil {
   261  			return err
   262  		}
   263  	}
   264  
   265  	c.text = ctx.Bool("text")
   266  
   267  	if ctx.Bool("url") {
   268  		if len(args) != 0 {
   269  			return fmt.Errorf("--url flag and --arg argument are incompatible")
   270  		}
   271  		if c.method == POST {
   272  			return fmt.Errorf("--url flag is incompatible with POST")
   273  		}
   274  		if payload != "" {
   275  			return fmt.Errorf("--url flag and --json-payload argument are incompatible")
   276  		}
   277  		return c.parseEndpointAsURL(ctx)
   278  	}
   279  
   280  	return nil
   281  }
   282  
   283  func (c *CmdAPICall) parseEndpointAsURL(ctx *cli.Context) error {
   284  	const apiPath string = "/_/api/1.0/"
   285  
   286  	u, err := url.Parse(c.endpoint)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	values, err := url.ParseQuery(u.RawQuery)
   291  	if err != nil {
   292  		return err
   293  	}
   294  	// Check host later. During ParseArgv, the environment is not
   295  	// necessarily completely set.
   296  	c.parsedHost = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
   297  	// Allow use of 'keybase.io' out of convenience and make it
   298  	// equivalent to production URI.
   299  	if strings.EqualFold(c.parsedHost, "https://keybase.io") {
   300  		c.parsedHost = libkb.ProductionServerURI
   301  	}
   302  	if !strings.HasPrefix(u.Path, apiPath) {
   303  		return fmt.Errorf("URL path has to be API path: %s", apiPath)
   304  	}
   305  	c.endpoint = strings.TrimPrefix(u.Path, apiPath)
   306  	c.endpoint = strings.TrimSuffix(c.endpoint, ".json")
   307  	for k, vals := range values {
   308  		for _, v := range vals {
   309  			c.addArgument(keybase1.StringKVPair{Key: k, Value: v})
   310  		}
   311  	}
   312  	return nil
   313  }
   314  
   315  func (c *CmdAPICall) GetUsage() libkb.Usage {
   316  	return libkb.Usage{
   317  		Config: true,
   318  		API:    true,
   319  	}
   320  }