github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  // ===========================================================================