// If the error parameter is non-nil, sets the response status code appropriately and // writes a CouchDB-style JSON description to the body. func (h *handler) writeError(err error) { if err != nil { err = auth.OIDCToHTTPError(err) // Map OIDC/OAuth2 errors to HTTP form status, message := base.ErrorAsHTTPStatus(err) h.writeStatus(status, message) } }
// HTTP handler for a POST to _bulk_docs func (h *handler) handleBulkDocs() error { body, err := h.readJSON() if err != nil { return err } newEdits, ok := body["new_edits"].(bool) if !ok { newEdits = true } docs, ok := body["docs"].([]interface{}) if !ok { err = base.HTTPErrorf(http.StatusBadRequest, "missing 'docs' property") return err } h.db.ReserveSequences(uint64(len(docs))) result := make([]db.Body, 0, len(docs)) for _, item := range docs { doc := item.(map[string]interface{}) docid, _ := doc["_id"].(string) var err error var revid string if newEdits { if docid != "" { revid, err = h.db.Put(docid, doc) } else { docid, revid, err = h.db.Post(doc) } } else { revisions := db.ParseRevisions(doc) if revisions == nil { err = base.HTTPErrorf(http.StatusBadRequest, "Bad _revisions") } else { revid = revisions[0] err = h.db.PutExistingRev(docid, doc, revisions) } } status := db.Body{} if docid != "" { status["id"] = docid } if err != nil { code, msg := base.ErrorAsHTTPStatus(err) status["status"] = code status["error"] = base.CouchHTTPErrorName(code) status["reason"] = msg base.Logf("\tBulkDocs: Doc %q --> %d %s (%v)", docid, code, msg, err) err = nil // wrote it to output already; not going to return it } else { status["rev"] = revid } result = append(result, status) } h.writeJSONStatus(http.StatusCreated, result) return nil }
func (sc *ServerContext) installPrincipals(context *db.DatabaseContext, spec map[string]*db.PrincipalConfig, what string) error { for name, princ := range spec { isGuest := name == base.GuestUsername if isGuest { internalName := "" princ.Name = &internalName } else { princ.Name = &name } _, err := context.UpdatePrincipal(*princ, (what == "user"), isGuest) if err != nil { // A conflict error just means updatePrincipal didn't overwrite an existing user. if status, _ := base.ErrorAsHTTPStatus(err); status != http.StatusConflict { return fmt.Errorf("Couldn't create %s %q: %v", what, name, err) } } else if isGuest { base.Log(" Reset guest user to config") } else { base.Logf(" Created %s %q", what, name) } } return nil }
func TestDatabase(t *testing.T) { db := setupTestDB(t) defer tearDownTestDB(t, db) // Test creating & updating a document: log.Printf("Create rev 1...") body := Body{"key1": "value1", "key2": 1234} rev1id, err := db.Put("doc1", body) assertNoError(t, err, "Couldn't create document") assert.Equals(t, rev1id, body["_rev"]) assert.Equals(t, rev1id, "1-cb0c9a22be0e5a1b01084ec019defa81") log.Printf("Create rev 2...") body["key1"] = "new value" body["key2"] = int64(4321) rev2id, err := db.Put("doc1", body) body["_id"] = "doc1" assertNoError(t, err, "Couldn't update document") assert.Equals(t, rev2id, body["_rev"]) assert.Equals(t, rev2id, "2-488724414d0ed6b398d6d2aeb228d797") // Retrieve the document: log.Printf("Retrieve doc...") gotbody, err := db.Get("doc1") assertNoError(t, err, "Couldn't get document") assert.DeepEquals(t, gotbody, body) log.Printf("Retrieve rev 1...") gotbody, err = db.GetRev("doc1", rev1id, false, nil) assertNoError(t, err, "Couldn't get document with rev 1") assert.DeepEquals(t, gotbody, Body{"key1": "value1", "key2": 1234, "_id": "doc1", "_rev": rev1id}) log.Printf("Retrieve rev 2...") gotbody, err = db.GetRev("doc1", rev2id, false, nil) assertNoError(t, err, "Couldn't get document with rev") assert.DeepEquals(t, gotbody, body) gotbody, err = db.GetRev("doc1", "bogusrev", false, nil) status, _ := base.ErrorAsHTTPStatus(err) assert.Equals(t, status, 404) // Test the _revisions property: log.Printf("Check _revisions...") gotbody, err = db.GetRev("doc1", rev2id, true, nil) revisions := gotbody["_revisions"].(Body) assert.Equals(t, revisions["start"], 2) assert.DeepEquals(t, revisions["ids"], []string{"488724414d0ed6b398d6d2aeb228d797", "cb0c9a22be0e5a1b01084ec019defa81"}) // Test RevDiff: log.Printf("Check RevDiff...") missing, possible := db.RevDiff("doc1", []string{"1-cb0c9a22be0e5a1b01084ec019defa81", "2-488724414d0ed6b398d6d2aeb228d797"}) assert.True(t, missing == nil) assert.True(t, possible == nil) missing, possible = db.RevDiff("doc1", []string{"1-cb0c9a22be0e5a1b01084ec019defa81", "3-foo"}) assert.DeepEquals(t, missing, []string{"3-foo"}) assert.DeepEquals(t, possible, []string{"2-488724414d0ed6b398d6d2aeb228d797"}) missing, possible = db.RevDiff("nosuchdoc", []string{"1-cb0c9a22be0e5a1b01084ec019defa81", "3-foo"}) assert.DeepEquals(t, missing, []string{"1-cb0c9a22be0e5a1b01084ec019defa81", "3-foo"}) assert.True(t, possible == nil) // Test PutExistingRev: log.Printf("Check PutExistingRev...") body["_rev"] = "4-four" body["key1"] = "fourth value" body["key2"] = int64(4444) history := []string{"4-four", "3-three", "2-488724414d0ed6b398d6d2aeb228d797", "1-cb0c9a22be0e5a1b01084ec019defa81"} err = db.PutExistingRev("doc1", body, history) assertNoError(t, err, "PutExistingRev failed") // Retrieve the document: log.Printf("Check Get...") gotbody, err = db.Get("doc1") assertNoError(t, err, "Couldn't get document") assert.DeepEquals(t, gotbody, body) // Compact and check how many obsolete revs were deleted: revsDeleted, err := db.Compact() assertNoError(t, err, "Compact failed") assert.Equals(t, revsDeleted, 2) }
// HTTP handler for a POST to _bulk_docs func (h *handler) handleBulkDocs() error { body, err := h.readJSON() if err != nil { return err } newEdits, ok := body["new_edits"].(bool) if !ok { newEdits = true } userDocs, ok := body["docs"].([]interface{}) if !ok { err = base.HTTPErrorf(http.StatusBadRequest, "missing 'docs' property") return err } lenDocs := len(userDocs) // split out local docs, save them on their own localDocs := make([]interface{}, 0, lenDocs) docs := make([]interface{}, 0, lenDocs) for _, item := range userDocs { doc := item.(map[string]interface{}) docid, _ := doc["_id"].(string) if strings.HasPrefix(docid, "_local/") { localDocs = append(localDocs, doc) } else { docs = append(docs, doc) } } h.db.ReserveSequences(uint64(len(docs))) result := make([]db.Body, 0, len(docs)) for _, item := range docs { doc := item.(map[string]interface{}) docid, _ := doc["_id"].(string) var err error var revid string if newEdits { if docid != "" { revid, err = h.db.Put(docid, doc) } else { docid, revid, err = h.db.Post(doc) } } else { revisions := db.ParseRevisions(doc) if revisions == nil { err = base.HTTPErrorf(http.StatusBadRequest, "Bad _revisions") } else { revid = revisions[0] err = h.db.PutExistingRev(docid, doc, revisions) } } status := db.Body{} if docid != "" { status["id"] = docid } if err != nil { code, msg := base.ErrorAsHTTPStatus(err) status["status"] = code status["error"] = base.CouchHTTPErrorName(code) status["reason"] = msg base.Logf("\tBulkDocs: Doc %q --> %d %s (%v)", docid, code, msg, err) err = nil // wrote it to output already; not going to return it } else { status["rev"] = revid } result = append(result, status) } for _, item := range localDocs { doc := item.(map[string]interface{}) for k, v := range doc { doc[k] = base.FixJSONNumbers(v) } var err error var revid string offset := len("_local/") docid, _ := doc["_id"].(string) idslug := docid[offset:] revid, err = h.db.PutSpecial("local", idslug, doc) status := db.Body{} status["id"] = docid if err != nil { code, msg := base.ErrorAsHTTPStatus(err) status["status"] = code status["error"] = base.CouchHTTPErrorName(code) status["reason"] = msg base.Logf("\tBulkDocs: Local Doc %q --> %d %s (%v)", docid, code, msg, err) err = nil } else { status["rev"] = revid } result = append(result, status) } h.writeJSONStatus(http.StatusCreated, result) return nil }
// HTTP handler for a POST to _bulk_get // Request looks like POST /db/_bulk_get?revs=___&attachments=___ // where the boolean ?revs parameter adds a revision history to each doc // and the boolean ?attachments parameter includes attachment bodies. // The body of the request is JSON and looks like: // { // "docs": [ // {"id": "docid", "rev": "revid", "atts_since": [12,...]}, ... // ] // } func (h *handler) handleBulkGet() error { includeRevs := h.getBoolQuery("revs") includeAttachments := h.getBoolQuery("attachments") // If a client passes the HTTP header "Accept-Encoding: gzip" then the header "X-Accept-Part-Encoding: gzip" will be // ignored and the entire HTTP response will be gzip compressed. (aside from exception mentioned below for issue 1419) acceptGzipPartEncoding := strings.Contains(h.rq.Header.Get("X-Accept-Part-Encoding"), "gzip") acceptGzipEncoding := strings.Contains(h.rq.Header.Get("Accept-Encoding"), "gzip") canCompressParts := acceptGzipPartEncoding && !acceptGzipEncoding // Exception: if the user agent is empty or earlier than 1.2, and X-Accept-Part-Encoding=gzip, then we actually // DO want to compress the parts since the full response will not be gzipped, since those clients can't handle it. // See https://github.com/couchbase/sync_gateway/issues/1419 and encoded_response_writer.go userAgentVersion := NewUserAgentVersion(h.rq.Header.Get("User-Agent")) if userAgentVersion.IsBefore(1, 2) && acceptGzipPartEncoding { canCompressParts = true } body, err := h.readJSON() if err != nil { return err } docs, ok := body["docs"].([]interface{}) if !ok { internalerr := base.HTTPErrorf(http.StatusBadRequest, "missing 'docs' property") return internalerr } err = h.writeMultipart("mixed", func(writer *multipart.Writer) error { for _, item := range docs { var body db.Body var attsSince []string var err error doc := item.(map[string]interface{}) docid, _ := doc["id"].(string) revid := "" revok := true if doc["rev"] != nil { revid, revok = doc["rev"].(string) } if docid == "" || !revok { err = base.HTTPErrorf(http.StatusBadRequest, "Invalid doc/rev ID in _bulk_get") } else { if includeAttachments { if doc["atts_since"] != nil { raw, ok := doc["atts_since"].([]interface{}) if ok { attsSince = make([]string, len(raw)) for i := 0; i < len(raw); i++ { attsSince[i], ok = raw[i].(string) if !ok { break } } } if !ok { err = base.HTTPErrorf(http.StatusBadRequest, "Invalid atts_since") } } else { attsSince = []string{} } } } if err == nil { body, err = h.db.GetRev(docid, revid, includeRevs, attsSince) } if err != nil { // Report error in the response for this doc: status, reason := base.ErrorAsHTTPStatus(err) errStr := base.CouchHTTPErrorName(status) body = db.Body{"id": docid, "error": errStr, "reason": reason, "status": status} if revid != "" { body["rev"] = revid } } h.db.WriteRevisionAsPart(body, err != nil, canCompressParts, writer) } return nil }) return err }
// HTTP handler for _all_docs func (h *handler) handleAllDocs() error { // http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API includeDocs := h.getBoolQuery("include_docs") includeChannels := h.getBoolQuery("channels") includeAccess := h.getBoolQuery("access") && h.user == nil includeRevs := h.getBoolQuery("revs") includeSeqs := h.getBoolQuery("update_seq") // Get the doc IDs if this is a POST request: var explicitDocIDs []string if h.rq.Method == "POST" { input, err := h.readJSON() if err == nil { keys, ok := input["keys"].([]interface{}) explicitDocIDs = make([]string, len(keys)) for i := 0; i < len(keys); i++ { if explicitDocIDs[i], ok = keys[i].(string); !ok { break } } if !ok { err = base.HTTPErrorf(http.StatusBadRequest, "Bad/missing keys") } } } else if h.rq.Method == "GET" { keys := h.getQuery("keys") if len(keys) > 0 { var queryKeys []string err := json.Unmarshal([]byte(keys), &queryKeys) if err != nil { err = base.HTTPErrorf(http.StatusBadRequest, "Bad keys") } if len(queryKeys) > 0 { explicitDocIDs = queryKeys } } } // Get the set of channels the user has access to; nil if user is admin or has access to user "*" var availableChannels channels.TimedSet if h.user != nil { availableChannels = h.user.InheritedChannels() if availableChannels == nil { panic("no channels for user?") } if availableChannels.Contains(channels.UserStarChannel) { availableChannels = nil } } // Subroutines that filter a channel list down to the ones that the user has access to: filterChannels := func(channels []string) []string { if availableChannels == nil { return channels } dst := 0 for _, ch := range channels { if availableChannels.Contains(ch) { channels[dst] = ch dst++ } } if dst == 0 { return nil } return channels[0:dst] } filterChannelSet := func(channelMap channels.ChannelMap) []string { var result []string if availableChannels == nil { result = []string{} } for ch, rm := range channelMap { if availableChannels == nil || availableChannels.Contains(ch) { //Do not include channels doc removed from in this rev if rm == nil { result = append(result, ch) } } } return result } type allDocsRowValue struct { Rev string `json:"rev"` Channels []string `json:"channels,omitempty"` Access map[string]base.Set `json:"access,omitempty"` // for admins only } type allDocsRow struct { Key string `json:"key"` ID string `json:"id,omitempty"` Value *allDocsRowValue `json:"value,omitempty"` Doc db.Body `json:"doc,omitempty"` UpdateSeq uint64 `json:"update_seq,omitempty"` Error string `json:"error,omitempty"` Status int `json:"status,omitempty"` } // Subroutine that creates a response row for a document: totalRows := 0 createRow := func(doc db.IDAndRev, channels []string) *allDocsRow { row := &allDocsRow{Key: doc.DocID} value := allDocsRowValue{} // Filter channels to ones available to user, and bail out if inaccessible: if explicitDocIDs == nil { if channels = filterChannels(channels); channels == nil { return nil // silently skip this doc } } if explicitDocIDs != nil || includeDocs || includeAccess { // Fetch the document body and other metadata that lives with it: body, channelSet, access, roleAccess, _, _, err := h.db.GetRevAndChannels(doc.DocID, doc.RevID, includeRevs) if err != nil { row.Status, _ = base.ErrorAsHTTPStatus(err) return row } else if body["_removed"] != nil { row.Status = http.StatusForbidden return row } if explicitDocIDs != nil { if channels = filterChannelSet(channelSet); channels == nil { row.Status = http.StatusForbidden return row } doc.RevID = body["_rev"].(string) } if includeDocs { row.Doc = body } if includeAccess && (access != nil || roleAccess != nil) { value.Access = map[string]base.Set{} for userName, channels := range access { value.Access[userName] = channels.AsSet() } for roleName, channels := range roleAccess { value.Access["role:"+roleName] = channels.AsSet() } } } row.Value = &value row.ID = doc.DocID value.Rev = doc.RevID if includeSeqs { row.UpdateSeq = doc.Sequence } if includeChannels { row.Value.Channels = channels } return row } // Subroutine that writes a response entry for a document: writeDoc := func(doc db.IDAndRev, channels []string) bool { row := createRow(doc, channels) if row != nil { if row.Status >= 300 { row.Error = base.CouchHTTPErrorName(row.Status) } if totalRows > 0 { h.response.Write([]byte(",")) } totalRows++ h.addJSON(row) return true } return false } var options db.ForEachDocIDOptions options.Startkey = h.getQuery("startkey") options.Endkey = h.getQuery("endkey") options.Limit = h.getIntQuery("limit", 0) // Now it's time to actually write the response! lastSeq, _ := h.db.LastSequence() h.setHeader("Content-Type", "application/json") h.response.Write([]byte(`{"rows":[` + "\n")) if explicitDocIDs != nil { count := uint64(0) for _, docID := range explicitDocIDs { writeDoc(db.IDAndRev{DocID: docID, RevID: "", Sequence: 0}, nil) count++ if options.Limit > 0 && count == options.Limit { break } } } else { if err := h.db.ForEachDocID(writeDoc, options); err != nil { return err } } h.response.Write([]byte(fmt.Sprintf("],\n"+`"total_rows":%d,"update_seq":%d}`, totalRows, lastSeq))) return nil }
// HTTP handler for a POST to _bulk_get // Request looks like POST /db/_bulk_get?revs=___&attachments=___ // where the boolean ?revs parameter adds a revision history to each doc // and the boolean ?attachments parameter includes attachment bodies. // The body of the request is JSON and looks like: // { // "docs": [ // {"id": "docid", "rev": "revid", "atts_since": [12,...]}, ... // ] // } func (h *handler) handleBulkGet() error { includeRevs := h.getBoolQuery("revs") includeAttachments := h.getBoolQuery("attachments") canCompress := strings.Contains(h.rq.Header.Get("X-Accept-Part-Encoding"), "gzip") body, err := h.readJSON() if err != nil { return err } docs, ok := body["docs"].([]interface{}) if !ok { internalerr := base.HTTPErrorf(http.StatusBadRequest, "missing 'docs' property") return internalerr } err = h.writeMultipart("mixed", func(writer *multipart.Writer) error { for _, item := range docs { var body db.Body var attsSince []string var err error doc := item.(map[string]interface{}) docid, _ := doc["id"].(string) revid := "" revok := true if doc["rev"] != nil { revid, revok = doc["rev"].(string) } if docid == "" || !revok { err = base.HTTPErrorf(http.StatusBadRequest, "Invalid doc/rev ID in _bulk_get") } else { if includeAttachments { if doc["atts_since"] != nil { raw, ok := doc["atts_since"].([]interface{}) if ok { attsSince = make([]string, len(raw)) for i := 0; i < len(raw); i++ { attsSince[i], ok = raw[i].(string) if !ok { break } } } if !ok { err = base.HTTPErrorf(http.StatusBadRequest, "Invalid atts_since") } } else { attsSince = []string{} } } } if err == nil { body, err = h.db.GetRev(docid, revid, includeRevs, attsSince) } if err != nil { // Report error in the response for this doc: status, reason := base.ErrorAsHTTPStatus(err) errStr := base.CouchHTTPErrorName(status) body = db.Body{"id": docid, "error": errStr, "reason": reason, "status": status} if revid != "" { body["rev"] = revid } } h.db.WriteRevisionAsPart(body, err != nil, canCompress, writer) } return nil }) return err }
// If the error parameter is non-nil, sets the response status code appropriately and // writes a CouchDB-style JSON description to the body. func (h *handler) writeError(err error) { if err != nil { status, message := base.ErrorAsHTTPStatus(err) h.writeStatus(status, message) } }