github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/client/request/request.go (about) 1 package request 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/base64" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 14 "github.com/cozy/cozy-stack/pkg/safehttp" 15 ) 16 17 const defaultUserAgent = "go-cozy-client" 18 19 type ( 20 // Authorizer is an interface to represent any element that can be used as a 21 // token bearer. 22 Authorizer interface { 23 AuthHeader() string 24 RealtimeToken() string 25 } 26 27 // Headers is a map of strings used to represent HTTP headers 28 Headers map[string]string 29 30 // Options is a struct holding of the details of a request. 31 // 32 // The NoResponse field can be used in case the call's response if not used. In 33 // such cases, the response body is automatically closed. 34 Options struct { 35 Addr string 36 Domain string 37 Scheme string 38 Method string 39 Path string 40 Queries url.Values 41 Headers Headers 42 Body io.Reader 43 Authorizer Authorizer 44 ContentLength int64 45 NoResponse bool 46 47 DisableSecure bool 48 Client *http.Client 49 UserAgent string 50 ParseError func(res *http.Response, b []byte) error 51 } 52 53 // Error is the typical JSON-API error returned by the API 54 Error struct { 55 Status string `json:"status"` 56 Title string `json:"title"` 57 Detail string `json:"detail"` 58 } 59 ) 60 61 func (e *Error) Error() string { 62 if e.Detail == "" || e.Title == e.Detail { 63 return e.Title 64 } 65 return fmt.Sprintf("%s: %s", e.Title, e.Detail) 66 } 67 68 // BasicAuthorizer implements the HTTP basic auth for authorization. 69 type BasicAuthorizer struct { 70 Username string 71 Password string 72 } 73 74 // AuthHeader implemented the interface Authorizer. 75 func (b *BasicAuthorizer) AuthHeader() string { 76 auth := b.Username + ":" + b.Password 77 return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) 78 } 79 80 // RealtimeToken implemented the interface Authorizer. 81 func (b *BasicAuthorizer) RealtimeToken() string { 82 return "" 83 } 84 85 // BearerAuthorizer implements a placeholder authorizer if the token is already 86 // known. 87 type BearerAuthorizer struct { 88 Token string 89 } 90 91 // AuthHeader implemented the interface Authorizer. 92 func (b *BearerAuthorizer) AuthHeader() string { 93 return "Bearer " + b.Token 94 } 95 96 // RealtimeToken implemented the interface Authorizer. 97 func (b *BearerAuthorizer) RealtimeToken() string { 98 return b.Token 99 } 100 101 // Req performs a request with the specified request options. 102 func Req(opts *Options) (*http.Response, error) { 103 scheme := opts.Scheme 104 if scheme == "" { 105 scheme = "http" 106 } 107 var host string 108 if opts.Addr != "" { 109 host = opts.Addr 110 } else { 111 host = opts.Domain 112 } 113 u := url.URL{ 114 Scheme: scheme, 115 Host: host, 116 Path: opts.Path, 117 } 118 if opts.Queries != nil { 119 u.RawQuery = opts.Queries.Encode() 120 } 121 122 req, err := http.NewRequest(opts.Method, u.String(), opts.Body) 123 if err != nil { 124 return nil, err 125 } 126 127 req.Host = opts.Domain 128 if opts.ContentLength > 0 { 129 req.ContentLength = opts.ContentLength 130 } 131 for k, v := range opts.Headers { 132 req.Header.Add(k, v) 133 } 134 135 if opts.Authorizer != nil { 136 req.Header.Add("Authorization", opts.Authorizer.AuthHeader()) 137 } 138 139 ua := opts.UserAgent 140 if ua == "" { 141 ua = defaultUserAgent 142 } 143 144 req.Header.Add("User-Agent", ua) 145 146 client := opts.Client 147 if client == nil { 148 client = safehttp.DefaultClient 149 } 150 151 res, err := client.Do(req) 152 if err != nil { 153 return nil, err 154 } 155 156 if res.StatusCode < 200 || res.StatusCode >= 300 { 157 return res, parseError(opts, res) 158 } 159 160 if opts.NoResponse { 161 err = res.Body.Close() 162 if err != nil { 163 return nil, err 164 } 165 } 166 167 return res, nil 168 } 169 170 func parseError(opts *Options, res *http.Response) error { 171 b, err := io.ReadAll(res.Body) 172 if cerr := res.Body.Close(); err == nil && cerr != nil { 173 err = cerr 174 } 175 if err != nil { 176 return &Error{ 177 Status: http.StatusText(res.StatusCode), 178 Title: http.StatusText(res.StatusCode), 179 Detail: err.Error(), 180 } 181 } 182 if opts.ParseError == nil { 183 return &Error{ 184 Status: http.StatusText(res.StatusCode), 185 Title: http.StatusText(res.StatusCode), 186 Detail: string(b), 187 } 188 } 189 return opts.ParseError(res, b) 190 } 191 192 // ErrSSEParse is used when an error occurred while parsing the SSE stream. 193 var ErrSSEParse = errors.New("could not parse event stream") 194 195 // SSEEvent holds the data of a single SSE event. 196 type SSEEvent struct { 197 Name string 198 Data []byte 199 Error error 200 } 201 202 // ReadSSE reads and parse a SSE source from a bufio.Reader into a channel of 203 // SSEEvent. 204 func ReadSSE(r io.ReadCloser, ch chan *SSEEvent) { 205 var err error 206 defer func() { 207 if err != nil { 208 ch <- &SSEEvent{Error: err} 209 } 210 if errc := r.Close(); errc != nil && err == nil { 211 ch <- &SSEEvent{Error: errc} 212 } 213 close(ch) 214 }() 215 rb := bufio.NewReader(r) 216 var ev *SSEEvent 217 for { 218 var bs []byte 219 bs, err = rb.ReadBytes('\n') 220 if errors.Is(err, io.EOF) { 221 err = nil 222 return 223 } 224 if err != nil { 225 return 226 } 227 if bytes.Equal(bs, []byte("\r\n")) { 228 ev = nil 229 continue 230 } 231 if bytes.HasPrefix(bs, []byte(":")) { 232 // A colon as the first character of a line is in essence a comment, 233 // and is ignored. 234 continue 235 } 236 spl := bytes.SplitN(bs, []byte(": "), 2) 237 if len(spl) != 2 { 238 err = ErrSSEParse 239 return 240 } 241 k, v := string(spl[0]), bytes.TrimSpace(spl[1]) 242 switch k { 243 case "event": 244 ev = &SSEEvent{Name: string(v)} 245 case "data": 246 if ev == nil { 247 err = ErrSSEParse 248 return 249 } 250 ev.Data = v 251 ch <- ev 252 default: 253 err = ErrSSEParse 254 return 255 } 256 } 257 } 258 259 // ReadJSON reads the content of the specified ReadCloser and closes it. 260 func ReadJSON(r io.ReadCloser, data interface{}) error { 261 err := json.NewDecoder(r).Decode(&data) 262 if cerr := r.Close(); err == nil && cerr != nil { 263 err = cerr 264 } 265 return err 266 } 267 268 // WriteJSON returns an io.Reader from which a JSON encoded data can be read. 269 func WriteJSON(data interface{}) (io.Reader, error) { 270 buf, err := json.Marshal(data) 271 if err != nil { 272 return nil, err 273 } 274 return bytes.NewReader(buf), nil 275 }