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 }