...

Source file src/github.com/go-kivik/kivik/v4/couchdb/chttp/chttp.go

Documentation: github.com/go-kivik/kivik/v4/couchdb/chttp

     1  // Licensed under the Apache License, Version 2.0 (the "License"); you may not
     2  // use this file except in compliance with the License. You may obtain a copy of
     3  // the License at
     4  //
     5  //  http://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     9  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    10  // License for the specific language governing permissions and limitations under
    11  // the License.
    12  
    13  // Package chttp provides a minimal HTTP driver backend for communicating with
    14  // CouchDB servers.
    15  package chttp
    16  
    17  import (
    18  	"bytes"
    19  	"compress/gzip"
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/url"
    27  	"runtime"
    28  	"strings"
    29  	"sync"
    30  
    31  	"github.com/go-kivik/kivik/v4"
    32  	"github.com/go-kivik/kivik/v4/driver"
    33  	internal "github.com/go-kivik/kivik/v4/int/errors"
    34  )
    35  
    36  const typeJSON = "application/json"
    37  
    38  // The default userAgent values
    39  const (
    40  	userAgent = "Kivik"
    41  )
    42  
    43  // Client represents a client connection. It embeds an *http.Client
    44  type Client struct {
    45  	// UserAgents is appended to set the User-Agent header. Typically it should
    46  	// contain pairs of product name and version.
    47  	UserAgents []string
    48  
    49  	*http.Client
    50  
    51  	rawDSN   string
    52  	dsn      *url.URL
    53  	basePath string
    54  	authMU   sync.Mutex
    55  
    56  	// noGzip will be set to true if the server fails on gzip-encoded requests.
    57  	noGzip bool
    58  }
    59  
    60  // New returns a connection to a remote CouchDB server. If credentials are
    61  // included in the URL, requests will be authenticated using Cookie Auth. To
    62  // use HTTP BasicAuth or some other authentication mechanism, do not specify
    63  // credentials in the URL, and instead call the [Client.Auth] method later.
    64  //
    65  // options must not be nil.
    66  func New(client *http.Client, dsn string, options driver.Options) (*Client, error) {
    67  	dsnURL, err := parseDSN(dsn)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	user := dsnURL.User
    72  	dsnURL.User = nil
    73  	c := &Client{
    74  		Client:   client,
    75  		dsn:      dsnURL,
    76  		basePath: strings.TrimSuffix(dsnURL.Path, "/"),
    77  		rawDSN:   dsn,
    78  	}
    79  	var auth authenticator
    80  	if user != nil {
    81  		password, _ := user.Password()
    82  		auth = &cookieAuth{
    83  			Username: user.Username(),
    84  			Password: password,
    85  		}
    86  	}
    87  	opts := map[string]interface{}{}
    88  	options.Apply(opts)
    89  	options.Apply(c)
    90  	options.Apply(&auth)
    91  	if auth != nil {
    92  		if err := auth.Authenticate(c); err != nil {
    93  			return nil, err
    94  		}
    95  	}
    96  	return c, nil
    97  }
    98  
    99  func parseDSN(dsn string) (*url.URL, error) {
   100  	if dsn == "" {
   101  		return nil, &internal.Error{
   102  			Status: http.StatusBadRequest,
   103  			Err:    errors.New("no URL specified"),
   104  		}
   105  	}
   106  	if !strings.HasPrefix(dsn, "http://") && !strings.HasPrefix(dsn, "https://") {
   107  		dsn = "http://" + dsn
   108  	}
   109  	dsnURL, err := url.Parse(dsn)
   110  	if err != nil {
   111  		return nil, &internal.Error{Status: http.StatusBadRequest, Err: err}
   112  	}
   113  	if dsnURL.Path == "" {
   114  		dsnURL.Path = "/"
   115  	}
   116  	return dsnURL, nil
   117  }
   118  
   119  // DSN returns the unparsed DSN used to connect.
   120  func (c *Client) DSN() string {
   121  	return c.rawDSN
   122  }
   123  
   124  // Response represents a response from a CouchDB server.
   125  type Response struct {
   126  	*http.Response
   127  
   128  	// ContentType is the base content type, parsed from the response headers.
   129  	ContentType string
   130  }
   131  
   132  // DecodeJSON unmarshals the response body into i. This method consumes and
   133  // closes the response body.
   134  func DecodeJSON(r *http.Response, i interface{}) error {
   135  	defer CloseBody(r.Body)
   136  	if err := json.NewDecoder(r.Body).Decode(i); err != nil {
   137  		return &internal.Error{Status: http.StatusBadGateway, Err: err}
   138  	}
   139  	return nil
   140  }
   141  
   142  // DoJSON combines [Client.DoReq], [Client.ResponseError], and
   143  // [Response.DecodeJSON], and closes the response body.
   144  func (c *Client) DoJSON(ctx context.Context, method, path string, opts *Options, i interface{}) error {
   145  	res, err := c.DoReq(ctx, method, path, opts)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	if res.Body != nil {
   150  		defer CloseBody(res.Body)
   151  	}
   152  	if err = ResponseError(res); err != nil {
   153  		return err
   154  	}
   155  	err = DecodeJSON(res, i)
   156  	return err
   157  }
   158  
   159  func (c *Client) path(path string) string {
   160  	if c.basePath != "" {
   161  		return c.basePath + "/" + strings.TrimPrefix(path, "/")
   162  	}
   163  	return path
   164  }
   165  
   166  // fullPathMatches returns true if the target resolves to match path.
   167  func (c *Client) fullPathMatches(path, target string) bool {
   168  	p, err := url.Parse(path)
   169  	if err != nil {
   170  		// should be impossible
   171  		return false
   172  	}
   173  	p.RawQuery = ""
   174  	t := new(url.URL)
   175  	*t = *c.dsn // shallow copy
   176  	t.Path = c.path(target)
   177  	t.RawQuery = ""
   178  	return t.String() == p.String()
   179  }
   180  
   181  // NewRequest returns a new *http.Request to the CouchDB server, and the
   182  // specified path. The host, schema, etc, of the specified path are ignored.
   183  func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader, opts *Options) (*http.Request, error) {
   184  	fullPath := c.path(path)
   185  	reqPath, err := url.Parse(fullPath)
   186  	if err != nil {
   187  		return nil, &internal.Error{Status: http.StatusBadRequest, Err: err}
   188  	}
   189  	u := *c.dsn // Make a copy
   190  	u.Path = reqPath.Path
   191  	u.RawQuery = reqPath.RawQuery
   192  	compress, body := c.compressBody(u.String(), body, opts)
   193  	req, err := http.NewRequest(method, u.String(), body)
   194  	if err != nil {
   195  		return nil, &internal.Error{Status: http.StatusBadRequest, Err: err}
   196  	}
   197  	if compress {
   198  		req.Header.Add("Content-Encoding", "gzip")
   199  	}
   200  	req.Header.Add("User-Agent", c.userAgent())
   201  	return req.WithContext(ctx), nil
   202  }
   203  
   204  func (c *Client) shouldCompressBody(path string, body io.Reader, opts *Options) bool {
   205  	if c.noGzip || (opts != nil && opts.NoGzip) {
   206  		return false
   207  	}
   208  	// /_session only supports compression from CouchDB 3.2.
   209  	if c.fullPathMatches(path, "/_session") {
   210  		return false
   211  	}
   212  	if body == nil {
   213  		return false
   214  	}
   215  	return true
   216  }
   217  
   218  // compressBody compresses body with gzip compression if appropriate. It will
   219  // return true, and the compressed stream, or false, and the unaltered stream.
   220  func (c *Client) compressBody(path string, body io.Reader, opts *Options) (bool, io.Reader) {
   221  	if !c.shouldCompressBody(path, body, opts) {
   222  		return false, body
   223  	}
   224  	r, w := io.Pipe()
   225  	go func() {
   226  		if closer, ok := body.(io.Closer); ok {
   227  			defer closer.Close()
   228  		}
   229  		gz := gzip.NewWriter(w)
   230  		_, err := io.Copy(gz, body)
   231  		_ = gz.Close()
   232  		w.CloseWithError(err)
   233  	}()
   234  	return true, r
   235  }
   236  
   237  // DoReq does an HTTP request. An error is returned only if there was an error
   238  // processing the request. In particular, an error status code, such as 400
   239  // or 500, does _not_ cause an error to be returned.
   240  func (c *Client) DoReq(ctx context.Context, method, path string, opts *Options) (*http.Response, error) {
   241  	if method == "" {
   242  		return nil, errors.New("chttp: method required")
   243  	}
   244  	var body io.Reader
   245  	if opts != nil {
   246  		if opts.GetBody != nil {
   247  			var err error
   248  			opts.Body, err = opts.GetBody()
   249  			if err != nil {
   250  				return nil, err
   251  			}
   252  		}
   253  		if opts.Body != nil {
   254  			body = opts.Body
   255  			defer opts.Body.Close() // nolint: errcheck
   256  		}
   257  	}
   258  	req, err := c.NewRequest(ctx, method, path, body, opts)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	fixPath(req, path)
   263  	setHeaders(req, opts)
   264  	setQuery(req, opts)
   265  	if opts != nil {
   266  		req.GetBody = opts.GetBody
   267  	}
   268  
   269  	trace := ContextClientTrace(ctx)
   270  	if trace != nil {
   271  		trace.httpRequest(req)
   272  		trace.httpRequestBody(req)
   273  	}
   274  
   275  	response, err := c.Do(req)
   276  	if trace != nil {
   277  		trace.httpResponse(response)
   278  		trace.httpResponseBody(response)
   279  	}
   280  	return response, netError(err)
   281  }
   282  
   283  func netError(err error) error {
   284  	if err == nil {
   285  		return nil
   286  	}
   287  	if urlErr, ok := err.(*url.Error); ok {
   288  		// If this error was generated by EncodeBody, it may have an embedded
   289  		// status code (!= 500), which we should honor.
   290  		status := kivik.HTTPStatus(urlErr.Err)
   291  		if status == http.StatusInternalServerError {
   292  			status = http.StatusBadGateway
   293  		}
   294  		return &internal.Error{Status: status, Err: err}
   295  	}
   296  	if status := kivik.HTTPStatus(err); status != http.StatusInternalServerError {
   297  		return err
   298  	}
   299  	return &internal.Error{Status: http.StatusBadGateway, Err: err}
   300  }
   301  
   302  // fixPath sets the request's URL.RawPath to work with escaped characters in
   303  // paths.
   304  func fixPath(req *http.Request, path string) {
   305  	// Remove any query parameters
   306  	parts := strings.SplitN(path, "?", 2) // nolint:gomnd
   307  	req.URL.RawPath = "/" + strings.TrimPrefix(parts[0], "/")
   308  }
   309  
   310  // BodyEncoder returns a function which returns the encoded body. It is meant
   311  // to be used as a http.Request.GetBody value.
   312  func BodyEncoder(i interface{}) func() (io.ReadCloser, error) {
   313  	return func() (io.ReadCloser, error) {
   314  		return EncodeBody(i), nil
   315  	}
   316  }
   317  
   318  // EncodeBody JSON encodes i to an io.ReadCloser. If an encoding error
   319  // occurs, it will be returned on the next read.
   320  func EncodeBody(i interface{}) io.ReadCloser {
   321  	done := make(chan struct{})
   322  	r, w := io.Pipe()
   323  	go func() {
   324  		defer close(done)
   325  		var err error
   326  		switch t := i.(type) {
   327  		case []byte:
   328  			_, err = w.Write(t)
   329  		case json.RawMessage: // Only needed for Go 1.7
   330  			_, err = w.Write(t)
   331  		case string:
   332  			_, err = w.Write([]byte(t))
   333  		default:
   334  			err = json.NewEncoder(w).Encode(i)
   335  			switch err.(type) {
   336  			case *json.MarshalerError, *json.UnsupportedTypeError, *json.UnsupportedValueError:
   337  				err = &internal.Error{Status: http.StatusBadRequest, Err: err}
   338  			}
   339  		}
   340  		_ = w.CloseWithError(err)
   341  	}()
   342  	return &ebReader{
   343  		ReadCloser: r,
   344  		done:       done,
   345  	}
   346  }
   347  
   348  type ebReader struct {
   349  	io.ReadCloser
   350  	done <-chan struct{}
   351  }
   352  
   353  var _ io.ReadCloser = &ebReader{}
   354  
   355  func (r *ebReader) Close() error {
   356  	err := r.ReadCloser.Close()
   357  	<-r.done
   358  	return err
   359  }
   360  
   361  func setHeaders(req *http.Request, opts *Options) {
   362  	accept := typeJSON
   363  	contentType := typeJSON
   364  	if opts != nil {
   365  		if opts.Accept != "" {
   366  			accept = opts.Accept
   367  		}
   368  		if opts.ContentType != "" {
   369  			contentType = opts.ContentType
   370  		}
   371  		if opts.FullCommit {
   372  			req.Header.Add("X-Couch-Full-Commit", "true")
   373  		}
   374  		if opts.IfNoneMatch != "" {
   375  			inm := "\"" + strings.Trim(opts.IfNoneMatch, "\"") + "\""
   376  			req.Header.Set("If-None-Match", inm)
   377  		}
   378  		if opts.ContentLength != 0 {
   379  			req.ContentLength = opts.ContentLength
   380  		}
   381  		for k, v := range opts.Header {
   382  			if _, ok := req.Header[k]; !ok {
   383  				req.Header[k] = v
   384  			}
   385  		}
   386  	}
   387  	req.Header.Add("Accept", accept)
   388  	req.Header.Add("Content-Type", contentType)
   389  }
   390  
   391  func setQuery(req *http.Request, opts *Options) {
   392  	if opts == nil || len(opts.Query) == 0 {
   393  		return
   394  	}
   395  	if req.URL.RawQuery == "" {
   396  		req.URL.RawQuery = opts.Query.Encode()
   397  		return
   398  	}
   399  	req.URL.RawQuery = strings.Join([]string{req.URL.RawQuery, opts.Query.Encode()}, "&")
   400  }
   401  
   402  // DoError is the same as DoReq(), followed by checking the response error. This
   403  // method is meant for cases where the only information you need from the
   404  // response is the status code. It unconditionally closes the response body.
   405  func (c *Client) DoError(ctx context.Context, method, path string, opts *Options) (*http.Response, error) {
   406  	res, err := c.DoReq(ctx, method, path, opts)
   407  	if err != nil {
   408  		return res, err
   409  	}
   410  	if res.Body != nil {
   411  		defer CloseBody(res.Body)
   412  	}
   413  	err = ResponseError(res)
   414  	return res, err
   415  }
   416  
   417  // ETag returns the unquoted ETag value, and a bool indicating whether it was
   418  // found.
   419  func ETag(resp *http.Response) (string, bool) {
   420  	if resp == nil {
   421  		return "", false
   422  	}
   423  	etag, ok := resp.Header["Etag"]
   424  	if !ok {
   425  		etag, ok = resp.Header["ETag"] // nolint: staticcheck
   426  	}
   427  	if !ok {
   428  		return "", false
   429  	}
   430  	return strings.Trim(etag[0], `"`), ok
   431  }
   432  
   433  // GetRev extracts the revision from the response's Etag header, if found. If
   434  // not, it falls back to reading the revision from the _rev field of the
   435  // document itself, then restores resp.Body for re-reading.
   436  func GetRev(resp *http.Response) (string, error) {
   437  	rev, ok := ETag(resp)
   438  	if ok {
   439  		return rev, nil
   440  	}
   441  	if resp == nil || resp.Request == nil || resp.Request.Method == http.MethodHead {
   442  		return "", errors.New("unable to determine document revision")
   443  	}
   444  	reassembled, rev, err := ExtractRev(resp.Body)
   445  	resp.Body = reassembled
   446  	return rev, err
   447  }
   448  
   449  // ExtractRev extracts the _rev field from r, while reading into a buffer,
   450  // then returns a re-assembled ReadCloser, containing the buffer plus any unread
   451  // bytes still on the network, along with the document revision.
   452  //
   453  // When the ETag header is missing, which can happen, for example, when doing
   454  // a request with revs_info=true.  This means we need to look through the
   455  // body of the request for the revision. Fortunately, CouchDB tends to send
   456  // the _id and _rev fields first, so we shouldn't need to parse the entire
   457  // body. The important thing is that resp.Body must be restored, so that the
   458  // normal document scanning can take place as usual.
   459  func ExtractRev(rc io.ReadCloser) (io.ReadCloser, string, error) {
   460  	buf := &bytes.Buffer{}
   461  	tr := io.TeeReader(rc, buf)
   462  	rev, err := readRev(tr)
   463  	reassembled := struct {
   464  		io.Reader
   465  		io.Closer
   466  	}{
   467  		Reader: io.MultiReader(buf, rc),
   468  		Closer: rc,
   469  	}
   470  	if err != nil {
   471  		return reassembled, "", fmt.Errorf("unable to determine document revision: %w", err)
   472  	}
   473  	return reassembled, rev, nil
   474  }
   475  
   476  // readRev searches r for a `_rev` field, and returns its value without reading
   477  // the rest of the JSON stream.
   478  func readRev(r io.Reader) (string, error) {
   479  	dec := json.NewDecoder(r)
   480  	tk, err := dec.Token()
   481  	if err != nil {
   482  		return "", err
   483  	}
   484  	if tk != json.Delim('{') {
   485  		return "", fmt.Errorf("Expected %q token, found %q", '{', tk)
   486  	}
   487  	var val json.RawMessage
   488  	for dec.More() {
   489  		tk, err = dec.Token()
   490  		if err != nil {
   491  			return "", err
   492  		}
   493  		if tk == "_rev" {
   494  			tk, err = dec.Token()
   495  			if err != nil {
   496  				return "", err
   497  			}
   498  			if value, ok := tk.(string); ok {
   499  				return value, nil
   500  			}
   501  			return "", fmt.Errorf("found %q in place of _rev value", tk)
   502  		}
   503  		// Discard the value associated with the token
   504  		if err := dec.Decode(&val); err != nil {
   505  			return "", err
   506  		}
   507  	}
   508  
   509  	return "", errors.New("_rev key not found in response body")
   510  }
   511  
   512  func (c *Client) userAgent() string {
   513  	ua := fmt.Sprintf("%s/%s (Language=%s; Platform=%s/%s)",
   514  		userAgent, kivik.Version, runtime.Version(), runtime.GOARCH, runtime.GOOS)
   515  	return strings.Join(append([]string{ua}, c.UserAgents...), " ")
   516  }
   517  

View as plain text