github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/couchdb/proxy.go (about)

     1  package couchdb
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io"
     7  	"net/http"
     8  	"net/http/httputil"
     9  	"net/url"
    10  	"strings"
    11  
    12  	"github.com/cozy/cozy-stack/pkg/config/config"
    13  	"github.com/cozy/cozy-stack/pkg/prefixer"
    14  	"github.com/cozy/cozy-stack/pkg/realtime"
    15  	"github.com/labstack/echo/v4"
    16  )
    17  
    18  // Proxy generate a httputil.ReverseProxy which forwards the request to the
    19  // correct route.
    20  func Proxy(db prefixer.Prefixer, doctype, path string) *httputil.ReverseProxy {
    21  	couch := config.CouchCluster(db.DBCluster())
    22  	transport := config.CouchClient().Transport
    23  
    24  	director := func(req *http.Request) {
    25  		req.URL.Scheme = couch.URL.Scheme
    26  		req.URL.Host = couch.URL.Host
    27  		req.Header.Del(echo.HeaderAuthorization) // drop stack auth
    28  		req.Header.Del(echo.HeaderCookie)
    29  		req.Header.Del("Host")
    30  		req.URL.RawPath = "/" + makeDBName(db, doctype) + "/" + path
    31  		req.URL.Path, _ = url.PathUnescape(req.URL.RawPath)
    32  		if auth := couch.Auth; auth != nil {
    33  			if p, ok := auth.Password(); ok {
    34  				req.SetBasicAuth(auth.Username(), p)
    35  			}
    36  		}
    37  	}
    38  
    39  	return &httputil.ReverseProxy{
    40  		Director:  director,
    41  		Transport: transport,
    42  	}
    43  }
    44  
    45  // ProxyBulkDocs generates a httputil.ReverseProxy to forward the couchdb
    46  // request on the _bulk_docs endpoint. This endpoint is specific since it will
    47  // mutate many document in database, the stack has to read the response from
    48  // couch to emit the correct realtime events.
    49  func ProxyBulkDocs(db prefixer.Prefixer, doctype string, req *http.Request) (*httputil.ReverseProxy, *http.Request, error) {
    50  	body, err := io.ReadAll(req.Body)
    51  	if err != nil {
    52  		return nil, nil, err
    53  	}
    54  
    55  	var reqValue struct {
    56  		Docs     []JSONDoc `json:"docs"`
    57  		NewEdits *bool     `json:"new_edits"`
    58  	}
    59  
    60  	if err = json.Unmarshal(body, &reqValue); err != nil {
    61  		return nil, nil, echo.NewHTTPError(http.StatusBadRequest,
    62  			"request body is not valid JSON")
    63  	}
    64  
    65  	var docs []JSONDoc
    66  	for _, doc := range reqValue.Docs {
    67  		doc.Type = doctype
    68  		docs = append(docs, doc)
    69  	}
    70  
    71  	// reset body to proxy
    72  	req.Body = io.NopCloser(bytes.NewReader(body))
    73  
    74  	p := Proxy(db, doctype, "_bulk_docs")
    75  	p.Transport = &bulkTransport{
    76  		RoundTripper: p.Transport,
    77  		OnResponseRead: func(data []byte) {
    78  			type respValue struct {
    79  				ID    string `json:"id"`
    80  				Rev   string `json:"rev"`
    81  				OK    bool   `json:"ok"`
    82  				Error string `json:"error"`
    83  			}
    84  
    85  			// When using the 'new_edits' flag (like pouchdb), the couchdb response
    86  			// does not contain any value. We only rely on the request data and
    87  			// expect no error.
    88  			if reqValue.NewEdits != nil && !*reqValue.NewEdits {
    89  				for _, doc := range docs {
    90  					rev := doc.Rev()
    91  					var event string
    92  					if strings.HasPrefix(rev, "1-") {
    93  						event = realtime.EventCreate
    94  					} else {
    95  						event = realtime.EventUpdate
    96  					}
    97  					RTEvent(db, event, &doc, nil)
    98  				}
    99  			} else {
   100  				var respValues []*respValue
   101  				if err = json.Unmarshal(data, &respValues); err != nil {
   102  					return
   103  				}
   104  
   105  				for i, r := range respValues {
   106  					if r.Error != "" || !r.OK {
   107  						continue
   108  					}
   109  					doc := docs[i]
   110  					var event string
   111  
   112  					if doc.Rev() == "" {
   113  						event = realtime.EventCreate
   114  						doc.SetID(r.ID)
   115  					} else if doc.Get("_deleted") == true {
   116  						event = realtime.EventDelete
   117  					} else {
   118  						event = realtime.EventUpdate
   119  					}
   120  					doc.SetRev(r.Rev)
   121  					RTEvent(db, event, &doc, nil)
   122  				}
   123  			}
   124  		},
   125  	}
   126  
   127  	return p, req, nil
   128  }
   129  
   130  type bulkTransport struct {
   131  	http.RoundTripper
   132  	OnResponseRead func([]byte)
   133  }
   134  
   135  func (t *bulkTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
   136  	resp, err = t.RoundTripper.RoundTrip(req)
   137  	if err != nil {
   138  		return nil, newConnectionError(err)
   139  	}
   140  	defer func() {
   141  		if errc := resp.Body.Close(); err == nil && errc != nil {
   142  			err = errc
   143  		}
   144  	}()
   145  	b, err := io.ReadAll(resp.Body)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	if resp.StatusCode == http.StatusCreated {
   150  		go t.OnResponseRead(b)
   151  	}
   152  	resp.Body = io.NopCloser(bytes.NewReader(b))
   153  	return resp, nil
   154  }