From 59290376d902ee4fa11f078be516961dd79682da Mon Sep 17 00:00:00 2001 From: Paul Mietz Egli Date: Wed, 15 Apr 2015 14:11:45 -0700 Subject: [PATCH 1/3] fixes issue #1: slashes in IDs not encoded. Added code to check for non-design document doc IDs and substitute %2F for / in the name. Modified the newRequest function to construct a url.URL object that does not decode path elements (see notes at http://godoc.org/net/url#URL) --- couchdb.go | 13 ++++++++++++ couchdb_test.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ http.go | 31 ++++++++++++++++++++++++---- x_test.go | 8 ++++++-- 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/couchdb.go b/couchdb.go index 724a767..0a0bd3a 100644 --- a/couchdb.go +++ b/couchdb.go @@ -124,6 +124,19 @@ var getJsonKeys = []string{"open_revs", "atts_since"} // // http://docs.couchdb.org/en/latest/api/document/common.html?highlight=doc#get--db-docid func (db *DB) Get(id string, doc interface{}, opts Options) error { + // issue #1: slashes in document IDs need to be escaped. + // ref: http://wiki.apache.org/couchdb/HTTP_Document_API#line-75 + const DDOC_PREFIX = "_design" + segments := strings.Split(id, "/") + if len(segments) > 1 { + if segments[0] == DDOC_PREFIX { + // preferred encoding for design docs is _design/seg1%2Fseg2 + id = segments[0] + "/" + strings.Join(segments[1:], "%2F") + } else { + id = strings.Join(segments, "%2F") + } + } + path, err := optpath(opts, getJsonKeys, db.name, id) if err != nil { return err diff --git a/couchdb_test.go b/couchdb_test.go index 932847c..a37277a 100644 --- a/couchdb_test.go +++ b/couchdb_test.go @@ -202,6 +202,60 @@ func TestGetExistingDoc(t *testing.T) { check(t, "doc.Field", int64(999), doc.Field) } +func TestGetExistingDocWithSlashInId(t *testing.T) { + c := newTestClient(t) + c.Handle("GET /db/doc%2Fslash", func(resp ResponseWriter, req *Request) { + io.WriteString(resp, `{ + "_id": "doc", + "_rev": "1-619db7ba8551c0de3f3a178775509611", + "field": 999 + }`) + }) + + var doc testDocument + if err := c.DB("db").Get("doc/slash", &doc, nil); err != nil { + t.Fatal(err) + } + check(t, "doc.Rev", "1-619db7ba8551c0de3f3a178775509611", doc.Rev) + check(t, "doc.Field", int64(999), doc.Field) +} + +func TestGetDesignDoc(t *testing.T) { + c := newTestClient(t) + c.Handle("GET /db/_design/myddoc", func(resp ResponseWriter, req *Request) { + io.WriteString(resp, `{ + "_id": "doc", + "_rev": "1-619db7ba8551c0de3f3a178775509611", + "field": 999 + }`) + }) + + var doc testDocument + if err := c.DB("db").Get("_design/myddoc", &doc, nil); err != nil { + t.Fatal(err) + } + check(t, "doc.Rev", "1-619db7ba8551c0de3f3a178775509611", doc.Rev) + check(t, "doc.Field", int64(999), doc.Field) +} + +func TestGetDesignDocWithSlashInId(t *testing.T) { + c := newTestClient(t) + c.Handle("GET /db/_design/myddoc%2Fslashed", func(resp ResponseWriter, req *Request) { + io.WriteString(resp, `{ + "_id": "doc", + "_rev": "1-619db7ba8551c0de3f3a178775509611", + "field": 999 + }`) + }) + + var doc testDocument + if err := c.DB("db").Get("_design/myddoc/slashed", &doc, nil); err != nil { + t.Fatal(err) + } + check(t, "doc.Rev", "1-619db7ba8551c0de3f3a178775509611", doc.Rev) + check(t, "doc.Field", int64(999), doc.Field) +} + func TestGetNonexistingDoc(t *testing.T) { c := newTestClient(t) c.Handle("GET /db/doc", func(resp ResponseWriter, req *Request) { diff --git a/http.go b/http.go index b3b2a68..a8779bf 100644 --- a/http.go +++ b/http.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "net/http" "net/url" "reflect" @@ -48,10 +49,32 @@ func (t *transport) setAuth(a Auth) { } func (t *transport) newRequest(method, path string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, t.prefix+path, body) - if err != nil { - return nil, err + // workaround for https://github.com/golang/go/issues/5684 + // see also http://godoc.org/net/url#URL + parsed, _ := url.Parse(t.prefix + path) + pathcomp := strings.Split(path, "?") + + newurl := url.URL{ + Host: parsed.Host, + Scheme: parsed.Scheme, + Opaque: pathcomp[0], + } + if len(pathcomp) > 1 { + newurl.RawQuery = pathcomp[1] } + + req := &http.Request{ + Method: method, + Host: parsed.Host, + URL: &newurl, + Header: http.Header{ + "User-Agent": {"go-couchdb/1.0"}, + }, + } + if body != nil { + req.Body = ioutil.NopCloser(body) + } + t.mu.RLock() defer t.mu.RUnlock() if t.auth != nil { @@ -94,7 +117,7 @@ func path(segs ...string) string { r := "" for _, seg := range segs { r += "/" - r += url.QueryEscape(seg) + r += seg } return r } diff --git a/x_test.go b/x_test.go index 9fc6188..a0ae198 100644 --- a/x_test.go +++ b/x_test.go @@ -35,9 +35,13 @@ func (s *testClient) ClearHandlers() { } func (s *testClient) RoundTrip(req *Request) (*Response, error) { - handler, ok := s.handlers[req.Method+" "+req.URL.Path] + path := req.URL.Path + if path == "" { + path = req.URL.Opaque + } + handler, ok := s.handlers[req.Method+" "+path] if !ok { - s.t.Fatalf("unhandled request: %s %s", req.Method, req.URL.Path) + s.t.Fatalf("unhandled request: %s %s", req.Method, path) return nil, nil } recorder := httptest.NewRecorder() From ee5ba2b8d92a48415f68e39c42ac259c16ae1b0f Mon Sep 17 00:00:00 2001 From: Paul Mietz Egli Date: Mon, 20 Apr 2015 11:04:01 -0700 Subject: [PATCH 2/3] refactored encoding to a utility function; added encoding for database names --- couchdb.go | 31 ++++++++----------------- couchdb_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ http.go | 44 ++++++++++++++++++++++++++++++++---- 3 files changed, 109 insertions(+), 26 deletions(-) diff --git a/couchdb.go b/couchdb.go index 0a0bd3a..7c7bed7 100644 --- a/couchdb.go +++ b/couchdb.go @@ -65,7 +65,7 @@ func (c *Client) SetAuth(a Auth) { // already exists. A valid DB object is returned in all cases, even if the // request fails. func (c *Client) CreateDB(name string) (*DB, error) { - if _, err := c.closedRequest("PUT", path(name), nil); err != nil { + if _, err := c.closedRequest("PUT", path(encid(name)), nil); err != nil { return c.DB(name), err } return c.DB(name), nil @@ -124,20 +124,7 @@ var getJsonKeys = []string{"open_revs", "atts_since"} // // http://docs.couchdb.org/en/latest/api/document/common.html?highlight=doc#get--db-docid func (db *DB) Get(id string, doc interface{}, opts Options) error { - // issue #1: slashes in document IDs need to be escaped. - // ref: http://wiki.apache.org/couchdb/HTTP_Document_API#line-75 - const DDOC_PREFIX = "_design" - segments := strings.Split(id, "/") - if len(segments) > 1 { - if segments[0] == DDOC_PREFIX { - // preferred encoding for design docs is _design/seg1%2Fseg2 - id = segments[0] + "/" + strings.Join(segments[1:], "%2F") - } else { - id = strings.Join(segments, "%2F") - } - } - - path, err := optpath(opts, getJsonKeys, db.name, id) + path, err := optpath(opts, getJsonKeys, encid(db.name), encid(id)) if err != nil { return err } @@ -152,12 +139,12 @@ func (db *DB) Get(id string, doc interface{}, opts Options) error { // It is faster than an equivalent Get request because no body // has to be parsed. func (db *DB) Rev(id string) (string, error) { - return responseRev(db.closedRequest("HEAD", path(db.name, id), nil)) + return responseRev(db.closedRequest("HEAD", path(encid(db.name), encid(id)), nil)) } // Put stores a document into the given database. func (db *DB) Put(id string, doc interface{}, rev string) (newrev string, err error) { - path := revpath(rev, db.name, id) + path := revpath(rev, encid(db.name), encid(id)) // TODO: make it possible to stream encoder output somehow json, err := json.Marshal(doc) if err != nil { @@ -169,7 +156,7 @@ func (db *DB) Put(id string, doc interface{}, rev string) (newrev string, err er // Delete marks a document revision as deleted. func (db *DB) Delete(id, rev string) (newrev string, err error) { - path := revpath(rev, db.name, id) + path := revpath(rev, encid(db.name), encid(id)) return responseRev(db.closedRequest("DELETE", path, nil)) } @@ -188,7 +175,7 @@ type Members struct { // Security retrieves the security object of a database. func (db *DB) Security() (*Security, error) { secobj := new(Security) - resp, err := db.request("GET", path(db.name, "_security"), nil) + resp, err := db.request("GET", path(encid(db.name), "_security"), nil) if err != nil { return nil, err } @@ -206,7 +193,7 @@ func (db *DB) Security() (*Security, error) { func (db *DB) PutSecurity(secobj *Security) error { json, _ := json.Marshal(secobj) body := bytes.NewReader(json) - _, err := db.request("PUT", path(db.name, "_security"), body) + _, err := db.request("PUT", path(encid(db.name), "_security"), body) return err } @@ -226,7 +213,7 @@ func (db *DB) View(ddoc, view string, result interface{}, opts Options) error { if !strings.HasPrefix(ddoc, "_design/") { return errors.New("couchdb.View: design doc name must start with _design/") } - path, err := optpath(opts, viewJsonKeys, db.name, ddoc, "_view", view) + path, err := optpath(opts, viewJsonKeys, encid(db.name), ddoc, "_view", encid(view)) if err != nil { return err } @@ -246,7 +233,7 @@ func (db *DB) View(ddoc, view string, result interface{}, opts Options) error { // // http://docs.couchdb.org/en/latest/api/database/bulk-api.html#db-all-docs func (db *DB) AllDocs(result interface{}, opts Options) error { - path, err := optpath(opts, viewJsonKeys, db.name, "_all_docs") + path, err := optpath(opts, viewJsonKeys, encid(db.name), "_all_docs") if err != nil { return err } diff --git a/couchdb_test.go b/couchdb_test.go index a37277a..9c291b2 100644 --- a/couchdb_test.go +++ b/couchdb_test.go @@ -95,6 +95,18 @@ func TestCreateDB(t *testing.T) { check(t, "db.Name()", "db", db.Name()) } +func TestCreateDBWithSlashInId(t *testing.T) { + c := newTestClient(t) + c.Handle("PUT /user%2F12345", func(resp ResponseWriter, req *Request) {}) + + db, err := c.CreateDB("user/12345") + if err != nil { + t.Fatal(err) + } + + check(t, "db.Name()", "user/12345", db.Name()) +} + func TestDeleteDB(t *testing.T) { c := newTestClient(t) c.Handle("DELETE /db", func(resp ResponseWriter, req *Request) {}) @@ -103,6 +115,30 @@ func TestDeleteDB(t *testing.T) { } } +func TestEnsureDB(t *testing.T) { + c := newTestClient(t) + c.Handle("PUT /ensuredb", func(resp ResponseWriter, req *Request) {}) + + db, err := c.EnsureDB("ensuredb") + if err != nil { + t.Fatal(err) + } + + check(t, "db.Name()", "ensuredb", db.Name()) +} + +func TestEnsureDBWithSlashInName(t *testing.T) { + c := newTestClient(t) + c.Handle("PUT /ensuredb%2Fslash", func(resp ResponseWriter, req *Request) {}) + + db, err := c.EnsureDB("ensuredb/slash") + if err != nil { + t.Fatal(err) + } + + check(t, "db.Name()", "ensuredb/slash", db.Name()) +} + func TestAllDBs(t *testing.T) { c := newTestClient(t) c.Handle("GET /_all_dbs", func(resp ResponseWriter, req *Request) { @@ -315,6 +351,30 @@ func TestPut(t *testing.T) { check(t, "returned rev", "1-619db7ba8551c0de3f3a178775509611", rev) } +func TestPutWithSlashes(t *testing.T) { + c := newTestClient(t) + c.Handle("PUT /user%2F12345/todo%2F2711", func(resp ResponseWriter, req *Request) { + body, _ := ioutil.ReadAll(req.Body) + check(t, "request body", `{"field":999}`, string(body)) + + resp.Header().Set("ETag", `"1-619db7ba8551c0de3f3a178775509611"`) + resp.WriteHeader(StatusCreated) + io.WriteString(resp, `{ + "id": "doc", + "ok": true, + "rev": "1-619db7ba8551c0de3f3a178775509611" + }`) + }) + + doc := &testDocument{Field: 999} + rev, err := c.DB("user/12345").Put("todo/2711", doc, "") + if err != nil { + t.Fatal(err) + } + check(t, "returned rev", "1-619db7ba8551c0de3f3a178775509611", rev) +} + + func TestPutWithRev(t *testing.T) { c := newTestClient(t) c.Handle("PUT /db/doc", func(resp ResponseWriter, req *Request) { diff --git a/http.go b/http.go index a8779bf..12468de 100644 --- a/http.go +++ b/http.go @@ -51,6 +51,7 @@ func (t *transport) setAuth(a Auth) { func (t *transport) newRequest(method, path string, body io.Reader) (*http.Request, error) { // workaround for https://github.com/golang/go/issues/5684 // see also http://godoc.org/net/url#URL + // most of the request creation code was taken from net/http/request.go NewRequest() parsed, _ := url.Parse(t.prefix + path) pathcomp := strings.Split(path, "?") @@ -63,16 +64,33 @@ func (t *transport) newRequest(method, path string, body io.Reader) (*http.Reque newurl.RawQuery = pathcomp[1] } + rc, ok := body.(io.ReadCloser) + if !ok && body != nil { + rc = ioutil.NopCloser(body) + } + req := &http.Request{ - Method: method, - Host: parsed.Host, - URL: &newurl, + Method: method, + URL: &newurl, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Host: parsed.Host, Header: http.Header{ "User-Agent": {"go-couchdb/1.0"}, }, + Body: rc, } + if body != nil { - req.Body = ioutil.NopCloser(body) + switch v := body.(type) { + case *bytes.Buffer: + req.ContentLength = int64(v.Len()) + case *bytes.Reader: + req.ContentLength = int64(v.Len()) + case *strings.Reader: + req.ContentLength = int64(v.Len()) + } } t.mu.RLock() @@ -80,6 +98,7 @@ func (t *transport) newRequest(method, path string, body io.Reader) (*http.Reque if t.auth != nil { t.auth.AddAuth(req) } + return req, nil } @@ -175,6 +194,23 @@ func encopts(opts Options, jskeys []string) (string, error) { return buf.String(), nil } +func encid(id string) string { + // issue #1: slashes in document IDs need to be escaped. + // ref: http://wiki.apache.org/couchdb/HTTP_Document_API#line-75 + const DDOC_PREFIX = "_design" + segments := strings.Split(id, "/") + if len(segments) > 1 { + if segments[0] == DDOC_PREFIX { + // preferred encoding for design docs is _design/seg1%2Fseg2 + id = segments[0] + "/" + strings.Join(segments[1:], "%2F") + } else { + id = strings.Join(segments, "%2F") + } + } + + return id +} + func encval(w io.Writer, k string, v interface{}) error { if v == nil { return errors.New("value is nil") From edf0aca93bf5b4f907d3804f6b0f63924299c831 Mon Sep 17 00:00:00 2001 From: Paul Mietz Egli Date: Mon, 20 Apr 2015 11:52:19 -0700 Subject: [PATCH 3/3] added document and database name encoding to attachment and feed functions --- attachments.go | 8 ++++---- feeds.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/attachments.go b/attachments.go index 933772d..dac2323 100644 --- a/attachments.go +++ b/attachments.go @@ -27,7 +27,7 @@ func (db *DB) Attachment(docid, name, rev string) (*Attachment, error) { return nil, fmt.Errorf("couchdb.GetAttachment: empty attachment Name") } - resp, err := db.request("GET", revpath(rev, db.name, docid, name), nil) + resp, err := db.request("GET", revpath(rev, encid(db.name), encid(docid), name), nil) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func (db *DB) AttachmentMeta(docid, name, rev string) (*Attachment, error) { return nil, fmt.Errorf("couchdb.GetAttachment: empty attachment Name") } - path := revpath(rev, db.name, docid, name) + path := revpath(rev, encid(db.name), encid(docid), name) resp, err := db.closedRequest("HEAD", path, nil) if err != nil { return nil, err @@ -72,7 +72,7 @@ func (db *DB) PutAttachment(docid string, att *Attachment, rev string) (newrev s return rev, fmt.Errorf("couchdb.PutAttachment: nil attachment Body") } - path := revpath(rev, db.name, docid, att.Name) + path := revpath(rev, encid(db.name), encid(docid), att.Name) req, err := db.newRequest("PUT", path, att.Body) if err != nil { return rev, err @@ -100,7 +100,7 @@ func (db *DB) DeleteAttachment(docid, name, rev string) (newrev string, err erro return rev, fmt.Errorf("couchdb.PutAttachment: empty name") } - path := revpath(rev, db.name, docid, name) + path := revpath(rev, encid(db.name), encid(docid), name) resp, err := db.closedRequest("DELETE", path, nil) return responseRev(resp, err) } diff --git a/feeds.go b/feeds.go index abc51f9..4e988f6 100644 --- a/feeds.go +++ b/feeds.go @@ -140,7 +140,7 @@ type ChangesFeed struct { // // http://docs.couchdb.org/en/latest/api/database/changes.html#db-changes func (db *DB) Changes(options Options) (*ChangesFeed, error) { - path, err := optpath(options, nil, db.name, "_changes") + path, err := optpath(options, nil, encid(db.name), "_changes") if err != nil { return nil, err }