github.com/go-kivik/kivik/v4@v4.3.2/couchdb/chttp/chttp.go (about) 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 }