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