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  }