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 }