github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/client/client.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"io"
     8  	"net/http"
     9  	"sync"
    10  
    11  	"github.com/cozy/cozy-stack/client/auth"
    12  	"github.com/cozy/cozy-stack/client/request"
    13  )
    14  
    15  // ErrWrongPassphrase is used when the passphrase is wrong
    16  var ErrWrongPassphrase = errors.New("Unauthorized: wrong passphrase")
    17  
    18  // jsonAPIErrors is a group of errors. It is the error type returned by the
    19  // API.
    20  type jsonAPIErrors struct {
    21  	Errors []*request.Error `json:"errors"`
    22  }
    23  
    24  // jsonAPIDocument is a simple JSONAPI document used to un-serialized
    25  type jsonAPIDocument struct {
    26  	Data     *json.RawMessage `json:"data"`
    27  	Included *json.RawMessage `json:"included"`
    28  	Links    *json.RawMessage `json:"links"`
    29  }
    30  
    31  // Client encapsulates the element representing a typical connection to the
    32  // HTTP api of the cozy-stack.
    33  //
    34  // It holds the elements to authenticate a user, as well as the transport layer
    35  // used for all the calls to the stack.
    36  type Client struct {
    37  	Addr   string
    38  	Domain string
    39  	Scheme string
    40  	Client *http.Client
    41  
    42  	AuthClient  *auth.Client
    43  	AuthScopes  []string
    44  	AuthAccept  auth.UserAcceptFunc
    45  	AuthStorage auth.Storage
    46  	Authorizer  request.Authorizer
    47  
    48  	UserAgent string
    49  	Retries   int
    50  	Transport http.RoundTripper
    51  
    52  	authed bool
    53  	inited bool
    54  	initMu sync.Mutex
    55  	authMu sync.Mutex
    56  	auth   *auth.Request
    57  }
    58  
    59  func (c *Client) init() {
    60  	c.initMu.Lock()
    61  	defer c.initMu.Unlock()
    62  	if c.inited {
    63  		return
    64  	}
    65  	if c.Retries == 0 {
    66  		c.Retries = 3
    67  	}
    68  	if c.Transport == nil {
    69  		transport := http.DefaultTransport.(*http.Transport).Clone()
    70  		transport.Proxy = http.ProxyFromEnvironment
    71  		c.Transport = transport
    72  	}
    73  	if c.AuthStorage == nil {
    74  		c.AuthStorage = auth.NewFileStorage()
    75  	}
    76  	if c.Client == nil {
    77  		c.Client = &http.Client{
    78  			Transport: c.Transport,
    79  			CheckRedirect: func(req *http.Request, via []*http.Request) error {
    80  				return http.ErrUseLastResponse
    81  			},
    82  		}
    83  	}
    84  	c.inited = true
    85  }
    86  
    87  // Authenticate is used to authenticate a client via OAuth.
    88  func (c *Client) Authenticate() (request.Authorizer, error) {
    89  	c.authMu.Lock()
    90  	defer c.authMu.Unlock()
    91  	if c.authed {
    92  		return c.auth, nil
    93  	}
    94  	if c.auth == nil {
    95  		c.auth = &auth.Request{
    96  			ClientParams: c.AuthClient,
    97  			Scopes:       c.AuthScopes,
    98  			Domain:       c.Domain,
    99  			Scheme:       c.Scheme,
   100  			HTTPClient:   c.Client,
   101  			UserAgent:    c.UserAgent,
   102  			UserAccept:   c.AuthAccept,
   103  			Storage:      c.AuthStorage,
   104  		}
   105  	}
   106  	if err := c.auth.Authenticate(); err != nil {
   107  		return nil, err
   108  	}
   109  	c.authed = true
   110  	return c.auth, nil
   111  }
   112  
   113  // Req is used to perform a request to the stack given the ReqOptions passed.
   114  func (c *Client) Req(opts *request.Options) (*http.Response, error) {
   115  	c.init()
   116  	var err error
   117  	if c.Authorizer != nil {
   118  		opts.Authorizer = c.Authorizer
   119  	} else {
   120  		opts.Authorizer, err = c.Authenticate()
   121  	}
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	opts.Addr = c.Addr
   126  	if opts.Domain == "" {
   127  		opts.Domain = c.Domain
   128  	}
   129  	opts.Scheme = c.Scheme
   130  	opts.Client = c.Client
   131  	opts.UserAgent = c.UserAgent
   132  	opts.ParseError = parseJSONAPIError
   133  	return request.Req(opts)
   134  }
   135  
   136  func parseJSONAPIError(res *http.Response, b []byte) error {
   137  	var errs jsonAPIErrors
   138  	if err := json.Unmarshal(b, &errs); err != nil || errs.Errors == nil || len(errs.Errors) == 0 {
   139  		return &request.Error{
   140  			Status: http.StatusText(res.StatusCode),
   141  			Title:  http.StatusText(res.StatusCode),
   142  			Detail: string(b),
   143  		}
   144  	}
   145  	return errs.Errors[0]
   146  }
   147  
   148  func readJSONAPI(r io.Reader, data interface{}) (err error) {
   149  	defer func() {
   150  		if rc, ok := r.(io.ReadCloser); ok {
   151  			if cerr := rc.Close(); err == nil && cerr != nil {
   152  				err = cerr
   153  			}
   154  		}
   155  	}()
   156  	var doc jsonAPIDocument
   157  	decoder := json.NewDecoder(r)
   158  	if err = decoder.Decode(&doc); err != nil {
   159  		return err
   160  	}
   161  	if data != nil {
   162  		return json.Unmarshal(*doc.Data, &data)
   163  	}
   164  	return nil
   165  }
   166  
   167  func readJSONAPILinks(r io.Reader, included, links interface{}) (err error) {
   168  	defer func() {
   169  		if rc, ok := r.(io.ReadCloser); ok {
   170  			if cerr := rc.Close(); err == nil && cerr != nil {
   171  				err = cerr
   172  			}
   173  		}
   174  	}()
   175  	var doc jsonAPIDocument
   176  	decoder := json.NewDecoder(r)
   177  	if err = decoder.Decode(&doc); err != nil {
   178  		return err
   179  	}
   180  	if included != nil && doc.Included != nil {
   181  		if err = json.Unmarshal(*doc.Included, &included); err != nil {
   182  			return err
   183  		}
   184  	}
   185  	if links != nil && doc.Links != nil {
   186  		if err = json.Unmarshal(*doc.Links, &links); err != nil {
   187  			return err
   188  		}
   189  	}
   190  	return nil
   191  }
   192  
   193  func writeJSONAPI(data interface{}) (io.Reader, error) {
   194  	buf, err := json.Marshal(data)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	doc := jsonAPIDocument{
   200  		Data: (*json.RawMessage)(&buf),
   201  	}
   202  	buf, err = json.Marshal(doc)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	return bytes.NewReader(buf), nil
   208  }