Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes issue #1: slashes in IDs not encoded. #3

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions attachments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
18 changes: 9 additions & 9 deletions couchdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -124,7 +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 {
path, err := optpath(opts, getJsonKeys, db.name, id)
path, err := optpath(opts, getJsonKeys, encid(db.name), encid(id))
if err != nil {
return err
}
Expand All @@ -139,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 {
Expand All @@ -156,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))
}

Expand All @@ -175,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
}
Expand All @@ -193,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
}

Expand All @@ -213,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
}
Expand All @@ -233,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
}
Expand Down
114 changes: 114 additions & 0 deletions couchdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {})
Expand All @@ -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) {
Expand Down Expand Up @@ -202,6 +238,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) {
Expand Down Expand Up @@ -261,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) {
Expand Down
2 changes: 1 addition & 1 deletion feeds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
67 changes: 63 additions & 4 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
Expand Down Expand Up @@ -48,15 +49,56 @@ 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
// most of the request creation code was taken from net/http/request.go NewRequest()
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]
}

rc, ok := body.(io.ReadCloser)
if !ok && body != nil {
rc = ioutil.NopCloser(body)
}

req := &http.Request{
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 {
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()
defer t.mu.RUnlock()
if t.auth != nil {
t.auth.AddAuth(req)
}

return req, nil
}

Expand Down Expand Up @@ -94,7 +136,7 @@ func path(segs ...string) string {
r := ""
for _, seg := range segs {
r += "/"
r += url.QueryEscape(seg)
r += seg
}
return r
}
Expand Down Expand Up @@ -152,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")
Expand Down
8 changes: 6 additions & 2 deletions x_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down