1// Copyright 2016 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package client
5
6import (
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
19type httpMethod int
20
21const (
22	GET httpMethod = iota
23	POST
24	DELETE
25)
26
27func (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
39type 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
53func 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
100func (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
151func (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
160func (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
168func (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
176func (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
185func (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
198func (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
207func (c *CmdAPICall) addArgument(arg keybase1.StringKVPair) {
208	c.args = append(c.args, arg)
209}
210
211type JSONInput map[string]json.RawMessage
212
213func (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
228func (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
283func (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
315func (c *CmdAPICall) GetUsage() libkb.Usage {
316	return libkb.Usage{
317		Config: true,
318		API:    true,
319	}
320}
321