github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/api.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package libkb 5 6 import ( 7 "bufio" 8 "bytes" 9 "encoding/base64" 10 "encoding/json" 11 "fmt" 12 "io" 13 "net/http" 14 "net/url" 15 "runtime" 16 "strings" 17 "sync" 18 "time" 19 20 "golang.org/x/net/context" 21 "golang.org/x/net/context/ctxhttp" 22 23 "github.com/PuerkitoBio/goquery" 24 jsonw "github.com/keybase/go-jsonw" 25 ) 26 27 // Shared code across Internal and External APIs 28 type BaseAPIEngine struct { 29 Contextified 30 config *ClientConfig 31 clientsMu sync.Mutex 32 clients map[int]*Client 33 } 34 35 type InternalAPIEngine struct { 36 BaseAPIEngine 37 } 38 39 type ExternalAPIEngine struct { 40 BaseAPIEngine 41 } 42 43 type AppStatusEmbed struct { 44 Status AppStatus `json:"status"` 45 } 46 47 func (s *AppStatusEmbed) GetAppStatus() *AppStatus { 48 return &s.Status 49 } 50 51 // Internal and External APIs both implement these methods, 52 // allowing us to share the request-making code below in doRequest 53 type Requester interface { 54 fixHeaders(m MetaContext, arg APIArg, req *http.Request, nist *NIST) error 55 getCli(needSession bool) (*Client, error) 56 consumeHeaders(m MetaContext, resp *http.Response, nist *NIST) error 57 isExternal() bool 58 } 59 60 // NewInternalAPIEngine makes an API engine for internally querying the keybase 61 // API server 62 func NewInternalAPIEngine(g *GlobalContext) (*InternalAPIEngine, error) { 63 cliConfig, err := genClientConfigForInternalAPI(g) 64 if err != nil { 65 return nil, err 66 } 67 68 i := &InternalAPIEngine{ 69 BaseAPIEngine{ 70 config: cliConfig, 71 clients: make(map[int]*Client), 72 Contextified: NewContextified(g), 73 }, 74 } 75 return i, nil 76 } 77 78 // Make a new InternalApiEngine and a new ExternalApiEngine, which share the 79 // same network config (i.e., TOR and Proxy parameters) 80 func NewAPIEngines(g *GlobalContext) (*InternalAPIEngine, *ExternalAPIEngine, error) { 81 i, err := NewInternalAPIEngine(g) 82 if err != nil { 83 return nil, nil, err 84 } 85 scraperConfig, err := genClientConfigForScrapers(g.Env) 86 if err != nil { 87 return nil, nil, err 88 } 89 x := &ExternalAPIEngine{ 90 BaseAPIEngine{ 91 config: scraperConfig, 92 clients: make(map[int]*Client), 93 Contextified: NewContextified(g), 94 }, 95 } 96 return i, x, nil 97 } 98 99 type APIStatus struct { 100 Code int `json:"code"` 101 Name string `json:"name"` 102 } 103 104 // ============================================================================ 105 // Errors 106 107 type APIError struct { 108 Msg string 109 Code int 110 } 111 112 func NewAPIErrorFromHTTPResponse(r *http.Response) *APIError { 113 return &APIError{r.Status, r.StatusCode} 114 } 115 116 func (a *APIError) Error() string { 117 if len(a.Msg) > 0 { 118 return a.Msg 119 } else if a.Code > 0 { 120 return fmt.Sprintf("Error HTTP status %d", a.Code) 121 } else { 122 return "Generic API error" 123 } 124 } 125 126 // Errors 127 // ============================================================================ 128 129 // ============================================================================ 130 // BaseApiEngine 131 132 func (api *BaseAPIEngine) getCli(cookied bool) (ret *Client, err error) { 133 key := 0 134 if cookied { 135 key |= 1 136 } 137 api.clientsMu.Lock() 138 client, found := api.clients[key] 139 if !found { 140 api.G().Log.Debug("| Cli wasn't found; remaking for cookied=%v", cookied) 141 client, err = NewClient(api.G(), api.config, cookied) 142 if err != nil { 143 return nil, err 144 } 145 api.clients[key] = client 146 } 147 api.clientsMu.Unlock() 148 return client, err 149 } 150 151 func HeaderVersion() string { 152 return GoClientID + " v" + VersionString() + " " + GetPlatformString() 153 } 154 155 func (api *BaseAPIEngine) PrepareGet(url1 url.URL, arg APIArg) (*http.Request, error) { 156 url1.RawQuery = arg.getHTTPArgs().Encode() 157 ruri := url1.String() 158 return http.NewRequest("GET", ruri, nil) 159 } 160 161 func (api *BaseAPIEngine) PrepareMethodWithBody(method string, url1 url.URL, arg APIArg) (*http.Request, error) { 162 ruri := url1.String() 163 var body io.Reader 164 165 useHTTPArgs := len(arg.getHTTPArgs()) > 0 166 useJSON := len(arg.JSONPayload) > 0 167 168 if useHTTPArgs && useJSON { 169 panic("PrepareMethodWithBody: Malformed APIArg: Both HTTP args and JSONPayload set on request.") 170 } 171 172 if useJSON { 173 jsonString, err := json.Marshal(arg.JSONPayload) 174 if err != nil { 175 return nil, err 176 } 177 body = bytes.NewReader(jsonString) 178 } else { 179 body = strings.NewReader(arg.getHTTPArgs().Encode()) 180 } 181 182 req, err := http.NewRequest(method, ruri, body) 183 if err != nil { 184 return nil, err 185 } 186 187 var typ string 188 if useJSON { 189 typ = "application/json" 190 } else { 191 typ = "application/x-www-form-urlencoded; charset=utf-8" 192 } 193 194 req.Header.Set("Content-Type", typ) 195 return req, nil 196 } 197 198 // 199 // ============================================================================ 200 201 type countingReader struct { 202 r io.Reader 203 n int 204 } 205 206 func newCountingReader(r io.Reader) *countingReader { 207 return &countingReader{r: r} 208 } 209 210 func (c *countingReader) Read(p []byte) (n int, err error) { 211 n, err = c.r.Read(p) 212 c.n += n 213 return n, err 214 } 215 216 func (c *countingReader) numRead() int { 217 return c.n 218 } 219 220 // ============================================================================ 221 // Shared code 222 // 223 224 func noopFinisher() {} 225 226 func getNIST(m MetaContext, sessType APISessionType) *NIST { 227 if sessType == APISessionTypeNONE { 228 return nil 229 } 230 231 if !m.G().Env.GetTorMode().UseSession() { 232 return nil 233 } 234 235 nist, err := m.NIST() 236 if nist == nil { 237 m.Debug("active device couldn't generate a NIST") 238 return nil 239 } 240 241 if err != nil { 242 m.Debug("Error generating NIST: %s", err) 243 return nil 244 } 245 return nist 246 } 247 248 // doRequestShared returns an http.Response, which is a live streaming object that 249 // escapes the function in which it was created. It therefore also returns 250 // a `finisher func()` that *must always be called* after the response is no longer 251 // needed. This finisher is always non-nil (and just a noop in some cases), 252 // so therefore it's fine to call it without checking for nil-ness. 253 func doRequestShared(m MetaContext, api Requester, arg APIArg, req *http.Request, wantJSONRes bool) (_ *http.Response, finisher func(), jw *jsonw.Wrapper, err error) { 254 m = m.EnsureCtx().WithLogTag("API") 255 defer m.Trace("api.doRequestShared", &err)() 256 m, tbs := m.WithTimeBuckets() 257 defer tbs.Record("API.request")() // note this doesn't include time reading body from GetResp 258 259 if !m.G().Env.GetTorMode().UseSession() && arg.SessionType == APISessionTypeREQUIRED { 260 err = TorSessionRequiredError{} 261 return 262 } 263 264 finisher = noopFinisher 265 266 nist := getNIST(m, arg.SessionType) 267 268 if err = api.fixHeaders(m, arg, req, nist); err != nil { 269 m.Debug("- API %s %s: fixHeaders error: %s", req.Method, req.URL, err) 270 return 271 } 272 needSession := false 273 if arg.SessionType != APISessionTypeNONE { 274 needSession = true 275 } 276 cli, err := api.getCli(needSession) 277 if err != nil { 278 return 279 } 280 281 // Actually send the request via Go's libraries 282 timerType := TimerAPI 283 if api.isExternal() { 284 timerType = TimerXAPI 285 } 286 287 var jsonBytes int 288 var status string 289 defer func() { 290 m.Debug("- API %s %s: err=%s, status=%q, jsonwBytes=%d", req.Method, req.URL, ErrToOk(err), status, jsonBytes) 291 }() 292 293 if m.G().Env.GetAPIDump() { 294 jpStr, _ := json.MarshalIndent(arg.JSONPayload, "", " ") 295 argStr, _ := json.MarshalIndent(arg.getHTTPArgs(), "", " ") 296 m.Debug("| full request: json:%s querystring:%s", jpStr, argStr) 297 } 298 299 timer := m.G().Timers.Start(timerType) 300 internalResp, canc, err := doRetry(m, arg, cli, req) 301 302 finisher = func() { 303 if internalResp != nil { 304 _ = DiscardAndCloseBody(internalResp) 305 internalResp = nil 306 } 307 if canc != nil { 308 canc() 309 canc = nil 310 } 311 } 312 313 defer func() { 314 if err != nil { 315 finisher() 316 finisher = noopFinisher 317 } 318 }() 319 320 timer.Report(req.Method + " " + arg.Endpoint) 321 322 if err != nil { 323 return nil, finisher, nil, APINetError{Err: err} 324 } 325 status = internalResp.Status 326 327 // The server sends "client version out of date" messages through the API 328 // headers. If the client is *really* out of date, the request status will 329 // be a 400 error, but these headers will still be present. So we need to 330 // handle headers *before* we abort based on status below. 331 err = api.consumeHeaders(m, internalResp, nist) 332 if err != nil { 333 return nil, finisher, nil, err 334 } 335 336 // Check for a code 200 or rather which codes were allowed in arg.HttpStatus 337 err = checkHTTPStatus(arg, internalResp) 338 if err != nil { 339 return nil, finisher, nil, err 340 } 341 342 if wantJSONRes { 343 var buf bytes.Buffer 344 bodyTee := io.TeeReader(internalResp.Body, &buf) 345 err = jsonw.EnsureMaxDepthDefault(bufio.NewReader(bodyTee)) 346 if err != nil { 347 return nil, finisher, nil, err 348 } 349 350 reader := newCountingReader(&buf) 351 decoder := json.NewDecoder(reader) 352 var obj interface{} 353 decoder.UseNumber() 354 err = decoder.Decode(&obj) 355 jsonBytes = reader.numRead() 356 if err != nil { 357 err = fmt.Errorf("Error in parsing JSON reply from server: %s", err) 358 return nil, finisher, nil, err 359 } 360 361 jw = jsonw.NewWrapper(obj) 362 if m.G().Env.GetAPIDump() { 363 b, _ := json.MarshalIndent(obj, "", " ") 364 m.Debug("| full reply: %s", b) 365 } 366 } 367 368 return internalResp, finisher, jw, nil 369 } 370 371 // doRetry will just call cli.cli.Do if arg.Timeout and arg.RetryCount aren't set. 372 // If they are set, it will cancel requests that last longer than arg.Timeout and 373 // retry them arg.RetryCount times. It returns 3 values: the HTTP response, if all goes 374 // well; a canceler function func() that the caller should call after all work is completed 375 // on this request; and an error. The canceler function is to clean up the timeout. 376 func doRetry(m MetaContext, arg APIArg, cli *Client, req *http.Request) (res *http.Response, cancel func(), err error) { 377 if m.G().Env.GetExtraNetLogging() { 378 defer m.Trace("api.doRetry", &err)() 379 } 380 381 // This serves as a proxy for checking the status of the Gregor connection. If we are not 382 // connected to Gregor, then it is likely the case we are totally offline, or on a very bad 383 // connection. If that is the case, let's make these timeouts very aggressive, so we don't 384 // block up everything trying to succeed when we probably will not. 385 if ConnectivityMonitorNo == m.G().ConnectivityMonitor.IsConnected(m.Ctx()) { 386 arg.InitialTimeout = HTTPFastTimeout 387 arg.RetryCount = 0 388 } 389 390 if arg.InitialTimeout == 0 && arg.RetryCount == 0 { 391 res, err = ctxhttp.Do(m.Ctx(), cli.cli, req) 392 return res, nil, err 393 } 394 395 timeout := cli.cli.Timeout 396 if arg.InitialTimeout != 0 { 397 timeout = arg.InitialTimeout 398 } 399 400 retries := 1 401 if arg.RetryCount > 1 { 402 retries = arg.RetryCount 403 } 404 405 multiplier := 1.0 406 if arg.RetryMultiplier != 0.0 { 407 multiplier = arg.RetryMultiplier 408 } 409 410 var lastErr error 411 for i := 0; i < retries; i++ { 412 if i > 0 { 413 m.Debug("retry attempt %d of %d for %s", i, retries, arg.Endpoint) 414 } 415 res, cancel, err = doTimeout(m, cli, req, timeout) 416 if err == nil { 417 return res, cancel, nil 418 } 419 lastErr = err 420 timeout = time.Duration(float64(timeout) * multiplier) 421 422 // If chat goes offline during this retry loop, then let's bail out early 423 if ConnectivityMonitorNo == m.G().ConnectivityMonitor.IsConnected(m.Ctx()) { 424 m.Debug("retry loop aborting since chat went offline") 425 break 426 } 427 428 if req.GetBody != nil { 429 // post request body consumed, need to get it back 430 req.Body, err = req.GetBody() 431 if err != nil { 432 return nil, nil, err 433 } 434 } 435 } 436 437 return nil, nil, fmt.Errorf("doRetry failed, attempts: %d, timeout %s, last err: %s", retries, timeout, lastErr) 438 } 439 440 // doTimeout does the http request with a timeout. It returns the response from making the HTTP request, 441 // a canceler, and an error. The canceler ought to be called before the caller (or its caller) is done 442 // with this request. 443 func doTimeout(m MetaContext, cli *Client, req *http.Request, timeout time.Duration) (res *http.Response, cancel func(), err error) { 444 if m.G().Env.GetExtraNetLogging() { 445 defer m.Trace("api.doTimeout", &err)() 446 } 447 // check to see if the current context is canceled 448 select { 449 case <-m.Ctx().Done(): 450 return nil, nil, m.Ctx().Err() 451 default: 452 } 453 ctx, cancel := context.WithTimeout(m.Ctx(), timeout*CITimeMultiplier(m.G())) 454 res, err = ctxhttp.Do(ctx, cli.cli, req) 455 return res, cancel, err 456 } 457 458 func checkHTTPStatus(arg APIArg, resp *http.Response) error { 459 var set []int 460 if arg.HTTPStatus == nil || len(arg.HTTPStatus) == 0 { 461 set = []int{200} 462 } else { 463 set = arg.HTTPStatus 464 } 465 for _, status := range set { 466 if resp.StatusCode == status { 467 return nil 468 } 469 } 470 return NewAPIErrorFromHTTPResponse(resp) 471 } 472 473 func (arg APIArg) getHTTPArgs() url.Values { 474 if arg.Args != nil { 475 return arg.Args.ToValues() 476 } 477 return arg.uArgs 478 } 479 480 func (arg APIArg) flattenHTTPArgs(args url.Values) map[string]string { 481 // HTTPArgs currently is a map of string -> [string] (with only one value). This is a helper to flatten this out 482 flatArgs := make(map[string]string) 483 484 for k, v := range args { 485 flatArgs[k] = v[0] 486 } 487 488 return flatArgs 489 } 490 491 // End shared code 492 // ============================================================================ 493 494 // ============================================================================ 495 // InternalApiEngine 496 497 func (a *InternalAPIEngine) getURL(arg APIArg, useText bool) url.URL { 498 u := *a.config.URL 499 var path string 500 if len(a.config.Prefix) > 0 { 501 path = a.config.Prefix 502 } else { 503 path = APIURIPathPrefix 504 } 505 u.Path = path + "/" + arg.Endpoint 506 if !useText { 507 u.Path += ".json" 508 } 509 return u 510 } 511 512 func (a *InternalAPIEngine) sessionArgs(m MetaContext, arg APIArg) (tok, csrf string, err error) { 513 if m.apiTokener != nil { 514 m.Debug("Using apiTokener session and CSRF token") 515 tok, csrf = m.apiTokener.Tokens() 516 return tok, csrf, nil 517 } 518 519 if tok, csrf := m.ProvisionalSessionArgs(); len(tok) > 0 && len(csrf) > 0 { 520 m.Debug("using provisional session args") 521 return tok, csrf, nil 522 } 523 return "", "", LoginRequiredError{"no sessionArgs available since no login path worked"} 524 } 525 526 func (a *InternalAPIEngine) isExternal() bool { return false } 527 528 func computeCriticalClockSkew(g *GlobalContext, s string) time.Duration { 529 var ret time.Duration 530 if s == "" { 531 return ret 532 } 533 serverNow, err := time.Parse(time.RFC1123, s) 534 535 if err != nil { 536 g.Log.Warning("Failed to parse server time: %s", err) 537 return ret 538 } 539 ourNow := g.Clock().Now() 540 diff := serverNow.Sub(ourNow) 541 if diff > CriticalClockSkewLimit || diff < -1*CriticalClockSkewLimit { 542 ret = diff 543 } 544 return ret 545 } 546 547 // If the local clock is within a reasonable offset of the server's 548 // clock, we'll get 0. Otherwise, we set the skew accordingly. Safe 549 // to set this every time. 550 func (a *InternalAPIEngine) updateCriticalClockSkewWarning(resp *http.Response) { 551 552 g := a.G() 553 g.oodiMu.RLock() 554 criticalClockSkew := int64(computeCriticalClockSkew(a.G(), resp.Header.Get("Date"))) 555 needUpdate := (criticalClockSkew != a.G().outOfDateInfo.CriticalClockSkew) 556 g.oodiMu.RUnlock() 557 558 if needUpdate { 559 g.oodiMu.Lock() 560 g.outOfDateInfo.CriticalClockSkew = criticalClockSkew 561 g.oodiMu.Unlock() 562 } 563 } 564 565 func (a *InternalAPIEngine) consumeHeaders(m MetaContext, resp *http.Response, nist *NIST) (err error) { 566 upgradeTo := resp.Header.Get("X-Keybase-Client-Upgrade-To") 567 upgradeURI := resp.Header.Get("X-Keybase-Upgrade-URI") 568 customMessage := resp.Header.Get("X-Keybase-Upgrade-Message") 569 if customMessage != "" { 570 decoded, err := base64.StdEncoding.DecodeString(customMessage) 571 if err == nil { 572 customMessage = string(decoded) 573 } else { 574 // If base64-decode fails, just log the error and skip decoding. 575 m.Error("Failed to decode X-Keybase-Upgrade-Message header: %s", err) 576 } 577 } 578 579 if nist != nil { 580 nistReply := resp.Header.Get("X-Keybase-Auth-NIST") 581 switch nistReply { 582 case "": 583 case "verified": 584 nist.MarkSuccess() 585 case "failed": 586 nist.MarkFailure() 587 m.Warning("NIST token failed to verify") 588 default: 589 m.Info("Unexpected 'X-Keybase-Auth-NIST' state: %s", nistReply) 590 } 591 } 592 593 a.updateCriticalClockSkewWarning(resp) 594 595 if len(upgradeTo) > 0 || len(customMessage) > 0 { 596 now := time.Now() 597 g := m.G() 598 g.oodiMu.Lock() 599 g.outOfDateInfo.UpgradeTo = upgradeTo 600 g.outOfDateInfo.UpgradeURI = upgradeURI 601 g.outOfDateInfo.CustomMessage = customMessage 602 if g.lastUpgradeWarning.IsZero() || now.Sub(*g.lastUpgradeWarning) > 3*time.Minute { 603 // Send the notification after we unlock 604 defer g.NotifyRouter.HandleClientOutOfDate(upgradeTo, upgradeURI, customMessage) 605 *g.lastUpgradeWarning = now 606 } 607 g.oodiMu.Unlock() 608 } else { 609 // We might be in a state where the server *used to* think we were out 610 // of date, but now it doesn't. (Maybe a bad config got pushed and then 611 // later fixed.) If so, we need to clear the global outOfDateInfo, so 612 // that the client stops printing warnings. 613 g := m.G() 614 g.oodiMu.Lock() 615 g.outOfDateInfo.UpgradeTo = "" 616 g.outOfDateInfo.UpgradeURI = "" 617 g.outOfDateInfo.CustomMessage = "" 618 g.oodiMu.Unlock() 619 } 620 return 621 } 622 623 func (a *InternalAPIEngine) fixHeaders(m MetaContext, arg APIArg, req *http.Request, nist *NIST) error { 624 625 if nist != nil { 626 req.Header.Set("X-Keybase-Session", nist.Token().String()) 627 } else if arg.SessionType != APISessionTypeNONE { 628 m.Debug("fixHeaders: falling back to legacy session management") 629 tok, csrf, err := a.sessionArgs(m, arg) 630 if err != nil { 631 if arg.SessionType == APISessionTypeREQUIRED { 632 m.Debug("fixHeaders: session required, but error getting sessionArgs: %s", err) 633 return err 634 } 635 m.Debug("fixHeaders: session optional, error getting sessionArgs: %s", err) 636 } 637 638 if m.G().Env.GetTorMode().UseSession() { 639 if len(tok) > 0 { 640 req.Header.Set("X-Keybase-Session", tok) 641 } else if arg.SessionType == APISessionTypeREQUIRED { 642 m.Warning("fixHeaders: need session, but session token empty") 643 return InternalError{Msg: "API request requires session, but session token empty"} 644 } 645 } 646 if m.G().Env.GetTorMode().UseCSRF() { 647 if len(csrf) > 0 { 648 req.Header.Set("X-CSRF-Token", csrf) 649 } else if arg.SessionType == APISessionTypeREQUIRED { 650 m.Warning("fixHeaders: need session, but session csrf empty") 651 return InternalError{Msg: "API request requires session, but session csrf empty"} 652 } 653 } 654 } 655 656 if m.G().Env.GetTorMode().UseHeaders() { 657 req.Header.Set("User-Agent", UserAgent) 658 identifyAs := HeaderVersion() 659 req.Header.Set("X-Keybase-Client", identifyAs) 660 if tags := LogTagsToString(m.Ctx()); tags != "" { 661 req.Header.Set("X-Keybase-Log-Tags", tags) 662 } 663 if arg.SessionType != APISessionTypeNONE { 664 if m.G().Env.GetDeviceID().Exists() { 665 req.Header.Set("X-Keybase-Device-ID", a.G().Env.GetDeviceID().String()) 666 } 667 if i := m.G().Env.GetInstallID(); i.Exists() { 668 req.Header.Set("X-Keybase-Install-ID", i.String()) 669 } 670 } 671 } 672 673 for k, v := range m.G().Env.Test.APIHeaders { 674 req.Header.Set(k, v) 675 } 676 677 return nil 678 } 679 680 func (a *InternalAPIEngine) checkAppStatusFromJSONWrapper(arg APIArg, jw *jsonw.Wrapper) (*AppStatus, error) { 681 var ast AppStatus 682 if err := jw.UnmarshalAgain(&ast); err != nil { 683 return nil, err 684 } 685 return &ast, a.checkAppStatus(arg, &ast) 686 } 687 688 func (a *InternalAPIEngine) checkAppStatus(arg APIArg, ast *AppStatus) error { 689 set := arg.AppStatusCodes 690 691 if len(set) == 0 { 692 set = []int{SCOk} 693 } 694 695 for _, status := range set { 696 if ast.Code == status { 697 return nil 698 } 699 } 700 701 return appStatusToTypedError(ast) 702 } 703 704 func appStatusToTypedError(ast *AppStatus) error { 705 switch ast.Code { 706 case SCBadSession: 707 return BadSessionError{"server rejected session; is your device revoked?"} 708 case SCFeatureFlag: 709 var feature Feature 710 if ast.Fields != nil { 711 if tmp, ok := ast.Fields["feature"]; ok { 712 feature = Feature(tmp) 713 } 714 } 715 return NewFeatureFlagError(ast.Desc, feature) 716 case SCTeamContactSettingsBlock: 717 return NewTeamContactSettingsBlockError(ast) 718 default: 719 return NewAppStatusError(ast) 720 } 721 } 722 723 func (a *InternalAPIEngine) Get(m MetaContext, arg APIArg) (*APIRes, error) { 724 url1 := a.getURL(arg, false) 725 req, err := a.PrepareGet(url1, arg) 726 if err != nil { 727 return nil, err 728 } 729 return a.DoRequest(m, arg, req) 730 } 731 732 // GetResp performs a GET request and returns the http response. The finisher 733 // second arg should be called whenever we're done with the response (if it's non-nil). 734 func (a *InternalAPIEngine) GetResp(m MetaContext, arg APIArg) (*http.Response, func(), error) { 735 m = m.EnsureCtx().WithLogTag("API") 736 737 url1 := a.getURL(arg, arg.UseText) 738 req, err := a.PrepareGet(url1, arg) 739 if err != nil { 740 return nil, noopFinisher, err 741 } 742 743 resp, finisher, _, err := doRequestShared(m, a, arg, req, false) 744 if err != nil { 745 return nil, finisher, err 746 } 747 748 return resp, finisher, nil 749 } 750 751 // GetDecode performs a GET request and decodes the response via 752 // JSON into the value pointed to by v. 753 func (a *InternalAPIEngine) GetDecode(m MetaContext, arg APIArg, v APIResponseWrapper) error { 754 m = m.EnsureCtx().WithLogTag("API") 755 return a.getDecode(m, arg, v) 756 } 757 758 func (a *InternalAPIEngine) GetDecodeCtx(ctx context.Context, arg APIArg, v APIResponseWrapper) error { 759 mctx := NewMetaContext(ctx, a.G()) 760 return a.GetDecode(mctx, arg, v) 761 } 762 763 func (a *InternalAPIEngine) getDecode(m MetaContext, arg APIArg, v APIResponseWrapper) error { 764 resp, finisher, err := a.GetResp(m, arg) 765 if err != nil { 766 m.Debug("| API GetDecode, GetResp error: %s", err) 767 return err 768 } 769 defer finisher() 770 771 reader := resp.Body.(io.Reader) 772 if a.G().Env.GetAPIDump() { 773 body, err := io.ReadAll(reader) 774 if err != nil { 775 return err 776 } 777 m.Debug("| response body: %s", string(body)) 778 reader = bytes.NewReader(body) 779 } 780 781 dec := json.NewDecoder(reader) 782 err = dec.Decode(&v) 783 if err != nil { 784 m.Debug("| API GetDecode, Decode error: %s", err) 785 return err 786 } 787 if err = a.checkAppStatus(arg, v.GetAppStatus()); err != nil { 788 m.Debug("| API GetDecode, checkAppStatus error: %s", err) 789 return err 790 } 791 792 return nil 793 } 794 795 func (a *InternalAPIEngine) Post(m MetaContext, arg APIArg) (*APIRes, error) { 796 url1 := a.getURL(arg, false) 797 req, err := a.PrepareMethodWithBody("POST", url1, arg) 798 if err != nil { 799 return nil, err 800 } 801 return a.DoRequest(m, arg, req) 802 } 803 804 // PostJSON does _not_ actually enforce the use of JSON. 805 // That is now determined by APIArg's fields. 806 func (a *InternalAPIEngine) PostJSON(m MetaContext, arg APIArg) (*APIRes, error) { 807 return a.Post(m, arg) 808 } 809 810 // postResp performs a POST request and returns the http response. 811 // The finisher() should be called after the response is no longer needed. 812 func (a *InternalAPIEngine) postResp(m MetaContext, arg APIArg) (*http.Response, func(), error) { 813 m = m.EnsureCtx().WithLogTag("API") 814 url1 := a.getURL(arg, false) 815 req, err := a.PrepareMethodWithBody("POST", url1, arg) 816 if err != nil { 817 return nil, nil, err 818 } 819 820 resp, finisher, _, err := doRequestShared(m, a, arg, req, false) 821 if err != nil { 822 return nil, finisher, err 823 } 824 825 return resp, finisher, nil 826 } 827 828 func (a *InternalAPIEngine) PostDecode(m MetaContext, arg APIArg, v APIResponseWrapper) error { 829 m = m.EnsureCtx().WithLogTag("API") 830 return a.postDecode(m, arg, v) 831 } 832 833 func (a *InternalAPIEngine) PostDecodeCtx(ctx context.Context, arg APIArg, v APIResponseWrapper) error { 834 m := NewMetaContext(ctx, a.G()) 835 m = m.EnsureCtx().WithLogTag("API") 836 return a.postDecode(m, arg, v) 837 } 838 839 func (a *InternalAPIEngine) postDecode(m MetaContext, arg APIArg, v APIResponseWrapper) error { 840 resp, finisher, err := a.postResp(m, arg) 841 if err != nil { 842 return err 843 } 844 defer finisher() 845 846 reader := resp.Body 847 dec := json.NewDecoder(reader) 848 err = dec.Decode(&v) 849 if err != nil { 850 return err 851 } 852 return a.checkAppStatus(arg, v.GetAppStatus()) 853 } 854 855 func (a *InternalAPIEngine) PostRaw(m MetaContext, arg APIArg, ctype string, r io.Reader) (*APIRes, error) { 856 url1 := a.getURL(arg, false) 857 req, err := http.NewRequest("POST", url1.String(), r) 858 if len(ctype) > 0 { 859 req.Header.Set("Content-Type", ctype) 860 } 861 if err != nil { 862 return nil, err 863 } 864 return a.DoRequest(m, arg, req) 865 } 866 867 func (a *InternalAPIEngine) Delete(m MetaContext, arg APIArg) (*APIRes, error) { 868 url1 := a.getURL(arg, false) 869 req, err := a.PrepareMethodWithBody("DELETE", url1, arg) 870 if err != nil { 871 return nil, err 872 } 873 return a.DoRequest(m, arg, req) 874 } 875 876 func (a *InternalAPIEngine) DoRequest(m MetaContext, arg APIArg, req *http.Request) (*APIRes, error) { 877 m = m.EnsureCtx().WithLogTag("API") 878 res, err := a.doRequest(m, arg, req) 879 return res, err 880 } 881 882 func (a *InternalAPIEngine) doRequest(m MetaContext, arg APIArg, req *http.Request) (res *APIRes, err error) { 883 m = m.EnsureCtx().WithLogTag("API") 884 resp, finisher, jw, err := doRequestShared(m, a, arg, req, true) 885 if err != nil { 886 return nil, err 887 } 888 // We have already consumed the response body here, no need to pass the 889 // size to finisher. 890 defer finisher() 891 892 status, err := jw.AtKey("status").ToDictionary() 893 if err != nil { 894 err = fmt.Errorf("Cannot parse server's 'status' field: %s", err) 895 return nil, err 896 } 897 898 // Check for an "OK" or whichever app-level replies were allowed by 899 // http.AppStatus 900 appStatus, err := a.checkAppStatusFromJSONWrapper(arg, status) 901 if err != nil { 902 m.Debug("- API call %s error: %s", arg.Endpoint, err) 903 return nil, err 904 } 905 906 body := jw 907 m.Debug("- API call %s success", arg.Endpoint) 908 return &APIRes{status, body, resp.StatusCode, appStatus}, err 909 } 910 911 // InternalApiEngine 912 // =========================================================================== 913 914 // =========================================================================== 915 // ExternalApiEngine 916 917 type XAPIResType int 918 919 const ( 920 XAPIResJSON XAPIResType = iota 921 XAPIResHTML 922 XAPIResText 923 ) 924 925 func (api *ExternalAPIEngine) fixHeaders(m MetaContext, arg APIArg, req *http.Request, nist *NIST) error { 926 // TODO (here and in the internal API engine implementation): If we don't 927 // set the User-Agent, it will default to http.defaultUserAgent 928 // ("Go-http-client/1.1"). We should think about whether that's what we 929 // want in Tor mode. Clients that are actually using Tor will always be 930 // distinguishable from the rest, insofar as their originating IP will be a 931 // Tor exit node, but there may be other use cases where this matters more? 932 userAgent := UserAgent 933 // Awful hack to make reddit as happy as possible. 934 if isReddit(req) { 935 userAgent += " (by /u/oconnor663)" 936 } else { 937 // For non-reddit sites we don't want to be served mobile HTML. 938 if runtime.GOOS == "android" { 939 userAgent = strings.Replace(userAgent, "android", "linux", 1) 940 } 941 } 942 if m.G().Env.GetTorMode().UseHeaders() { 943 req.Header.Set("User-Agent", userAgent) 944 } 945 946 return nil 947 } 948 949 func isReddit(req *http.Request) bool { 950 host := req.URL.Host 951 return host == "reddit.com" || strings.HasSuffix(host, ".reddit.com") 952 } 953 954 func (api *ExternalAPIEngine) consumeHeaders(m MetaContext, resp *http.Response, nist *NIST) error { 955 return nil 956 } 957 958 func (api *ExternalAPIEngine) isExternal() bool { return true } 959 960 func (api *ExternalAPIEngine) DoRequest(m MetaContext, 961 arg APIArg, req *http.Request, restype XAPIResType) ( 962 ar *ExternalAPIRes, hr *ExternalHTMLRes, tr *ExternalTextRes, err error) { 963 964 m = m.EnsureCtx().WithLogTag("API") 965 966 var resp *http.Response 967 var jw *jsonw.Wrapper 968 var finisher func() 969 970 wantJSONRes := (restype == XAPIResJSON) 971 resp, finisher, jw, err = doRequestShared(m, api, arg, req, wantJSONRes) 972 if err != nil { 973 return 974 } 975 defer finisher() 976 977 switch restype { 978 case XAPIResJSON: 979 ar = &ExternalAPIRes{resp.StatusCode, jw} 980 case XAPIResHTML: 981 var goq *goquery.Document 982 reader := newCountingReader(resp.Body) 983 goq, err = goquery.NewDocumentFromReader(reader) 984 if err == nil { 985 hr = &ExternalHTMLRes{resp.StatusCode, goq} 986 } 987 case XAPIResText: 988 var buf bytes.Buffer 989 _, err = buf.ReadFrom(resp.Body) 990 if err == nil { 991 tr = &ExternalTextRes{resp.StatusCode, buf.String()} 992 } 993 default: 994 err = fmt.Errorf("unknown restype to DoRequest") 995 } 996 return 997 } 998 999 func (api *ExternalAPIEngine) getCommon(m MetaContext, arg APIArg, restype XAPIResType) ( 1000 ar *ExternalAPIRes, hr *ExternalHTMLRes, tr *ExternalTextRes, err error) { 1001 1002 url1, err := url.Parse(arg.Endpoint) 1003 if err != nil { 1004 return nil, nil, nil, err 1005 } 1006 // If the specified endpoint has any query parameters attached, add them to 1007 // the uArgs. 1008 if arg.uArgs == nil { 1009 arg.uArgs = url1.Query() 1010 } else { 1011 for k, v := range url1.Query() { 1012 if _, ok := arg.uArgs[k]; ok { 1013 arg.uArgs[k] = append(arg.uArgs[k], v...) 1014 } else { 1015 arg.uArgs[k] = v 1016 } 1017 } 1018 } 1019 1020 req, err := api.PrepareGet(*url1, arg) 1021 if err != nil { 1022 return nil, nil, nil, err 1023 } 1024 1025 return api.DoRequest(m, arg, req, restype) 1026 } 1027 1028 func (api *ExternalAPIEngine) Get(m MetaContext, arg APIArg) (res *ExternalAPIRes, err error) { 1029 res, _, _, err = api.getCommon(m, arg, XAPIResJSON) 1030 return 1031 } 1032 1033 func (api *ExternalAPIEngine) GetHTML(m MetaContext, arg APIArg) (res *ExternalHTMLRes, err error) { 1034 _, res, _, err = api.getCommon(m, arg, XAPIResHTML) 1035 return 1036 } 1037 1038 func (api *ExternalAPIEngine) GetText(m MetaContext, arg APIArg) (res *ExternalTextRes, err error) { 1039 _, _, res, err = api.getCommon(m, arg, XAPIResText) 1040 return 1041 } 1042 1043 func (api *ExternalAPIEngine) postCommon(m MetaContext, arg APIArg, restype XAPIResType) ( 1044 ar *ExternalAPIRes, hr *ExternalHTMLRes, err error) { 1045 1046 var url1 *url.URL 1047 var req *http.Request 1048 url1, err = url1.Parse(arg.Endpoint) 1049 1050 if err != nil { 1051 return 1052 } 1053 req, err = api.PrepareMethodWithBody("POST", *url1, arg) 1054 if err != nil { 1055 return 1056 } 1057 1058 ar, hr, _, err = api.DoRequest(m, arg, req, restype) 1059 return 1060 } 1061 1062 func (api *ExternalAPIEngine) Post(m MetaContext, arg APIArg) (res *ExternalAPIRes, err error) { 1063 res, _, err = api.postCommon(m, arg, XAPIResJSON) 1064 return 1065 } 1066 1067 func (api *ExternalAPIEngine) PostHTML(m MetaContext, arg APIArg) (res *ExternalHTMLRes, err error) { 1068 _, res, err = api.postCommon(m, arg, XAPIResHTML) 1069 return 1070 } 1071 1072 // ExternalApiEngine 1073 // ===========================================================================