From aee6f18edf8cdf3f7c09c93fcf1af48c2c15fcd8 Mon Sep 17 00:00:00 2001 From: "Damien F. Katz" Date: Mon, 17 Nov 2008 18:18:51 +0000 Subject: More security and validation work. Still incomplete. git-svn-id: https://svn.apache.org/repos/asf/incubator/couchdb/trunk@718311 13f79535-47bb-0310-9956-ffa450edef68 --- share/www/script/couch.js | 181 ++++++++++++++++++++-------------------- share/www/script/couch_tests.js | 27 ++++-- 2 files changed, 112 insertions(+), 96 deletions(-) (limited to 'share') diff --git a/share/www/script/couch.js b/share/www/script/couch.js index cae0abc9..934dacf8 100644 --- a/share/www/script/couch.js +++ b/share/www/script/couch.js @@ -13,58 +13,64 @@ // A simple class to represent a database. Uses XMLHttpRequest to interface with // the CouchDB server. -function CouchDB(name, options) { +function CouchDB(name, httpHeaders) { this.name = name; this.uri = "/" + encodeURIComponent(name) + "/"; + + // The XMLHttpRequest object from the most recent request. Callers can + // use this to check result http status and headers. + this.last_req = null; + request = function(method, uri, requestOptions) { - return CouchDB.request(method, uri, combine(requestOptions, options)); + requestOptions = requestOptions || {} + requestOptions.headers = combine(requestOptions.headers, httpHeaders) + return CouchDB.request(method, uri, requestOptions); } // Creates the database on the server this.createDb = function() { - var req = request("PUT", this.uri); - maybeThrowError(req); - return JSON.parse(req.responseText); + this.last_req = request("PUT", this.uri); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); } // Deletes the database on the server this.deleteDb = function() { - var req = request("DELETE", this.uri); - if (req.status == 404) + this.last_req = request("DELETE", this.uri); + if (this.last_req.status == 404) return false; - maybeThrowError(req); - return JSON.parse(req.responseText); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); } // Save a document to the database this.save = function(doc, options) { - var req; if (doc._id == undefined) doc._id = CouchDB.newUuids(1)[0]; - req = request("PUT", this.uri + encodeURIComponent(doc._id) + encodeOptions(options), { - body: JSON.stringify(doc) - }); - maybeThrowError(req); - var result = JSON.parse(req.responseText); + this.last_req = request("PUT", this.uri + + encodeURIComponent(doc._id) + encodeOptions(options), + {body: JSON.stringify(doc)}); + CouchDB.maybeThrowError(this.last_req); + var result = JSON.parse(this.last_req.responseText); doc._rev = result.rev; return result; } // Open a document from the database this.open = function(docId, options) { - var req = request("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options)); - if (req.status == 404) + this.last_req = request("GET", this.uri + encodeURIComponent(docId) + encodeOptions(options)); + if (this.last_req.status == 404) return null; - maybeThrowError(req); - return JSON.parse(req.responseText); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); } // Deletes a document from the database this.deleteDoc = function(doc) { - var req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "?rev=" + doc._rev); - maybeThrowError(req); - var result = JSON.parse(req.responseText); + this.last_req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "?rev=" + doc._rev); + CouchDB.maybeThrowError(this.last_req); + var result = JSON.parse(this.last_req.responseText); doc._rev = result.rev; //record rev in input document doc._deleted = true; return result; @@ -72,9 +78,9 @@ function CouchDB(name, options) { // Deletes an attachment from a document this.deleteDocAttachment = function(doc, attachment_name) { - var req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "/" + attachment_name + "?rev=" + doc._rev); - maybeThrowError(req); - var result = JSON.parse(req.responseText); + this.last_req = request("DELETE", this.uri + encodeURIComponent(doc._id) + "/" + attachment_name + "?rev=" + doc._rev); + CouchDB.maybeThrowError(this.last_req); + var result = JSON.parse(this.last_req.responseText); doc._rev = result.rev; //record rev in input document return result; } @@ -92,11 +98,11 @@ function CouchDB(name, options) { if (docs[i]._id == undefined) docs[i]._id = newUuids.pop(); } - var req = request("POST", this.uri + "_bulk_docs" + encodeOptions(options), { + this.last_req = request("POST", this.uri + "_bulk_docs" + encodeOptions(options), { body: JSON.stringify({"docs": docs}) }); - maybeThrowError(req); - var result = JSON.parse(req.responseText); + CouchDB.maybeThrowError(this.last_req); + var result = JSON.parse(this.last_req.responseText); for (var i = 0; i < docs.length; i++) { docs[i]._rev = result.new_revs[i].rev; } @@ -117,49 +123,49 @@ function CouchDB(name, options) { reduceFun = reduceFun.toSource ? reduceFun.toSource() : "(" + reduceFun.toString() + ")"; body.reduce = reduceFun; } - var req = request("POST", this.uri + "_temp_view" + encodeOptions(options), { + this.last_req = request("POST", this.uri + "_temp_view" + encodeOptions(options), { headers: {"Content-Type": "application/json"}, body: JSON.stringify(body) }); - maybeThrowError(req); - return JSON.parse(req.responseText); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); } this.view = function(viewname, options, keys) { - var req = null ; if(!keys) { - req = request("GET", this.uri + "_view/" + viewname + encodeOptions(options)); + this.last_req = request("GET", this.uri + "_view/" + + viewname + encodeOptions(options)); } else { - req = request("POST", this.uri + "_view/" + viewname + encodeOptions(options), { + this.last_req = request("POST", this.uri + "_view/" + + viewname + encodeOptions(options), { headers: {"Content-Type": "application/json"}, body: JSON.stringify({keys:keys}) }); } - if (req.status == 404) + if (this.last_req.status == 404) return null; - maybeThrowError(req); - return JSON.parse(req.responseText); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); } // gets information about the database this.info = function() { - var req = request("GET", this.uri); - maybeThrowError(req); - return JSON.parse(req.responseText); + this.last_req = request("GET", this.uri); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); } this.allDocs = function(options,keys) { - var req = null; if(!keys) { - req = request("GET", this.uri + "_all_docs" + encodeOptions(options)); + this.last_req = request("GET", this.uri + "_all_docs" + encodeOptions(options)); } else { - req = request("POST", this.uri + "_all_docs" + encodeOptions(options), { + this.last_req = request("POST", this.uri + "_all_docs" + encodeOptions(options), { headers: {"Content-Type": "application/json"}, body: JSON.stringify({keys:keys}) }); } - maybeThrowError(req); - return JSON.parse(req.responseText); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); } this.allDocsBySeq = function(options,keys) { @@ -172,14 +178,14 @@ function CouchDB(name, options) { body: JSON.stringify({keys:keys}) }); } - maybeThrowError(req); + CouchDB.maybeThrowError(req); return JSON.parse(req.responseText); } this.compact = function() { - var req = request("POST", this.uri + "_compact"); - maybeThrowError(req); - return JSON.parse(req.responseText); + this.last_req = request("POST", this.uri + "_compact"); + CouchDB.maybeThrowError(this.last_req); + return JSON.parse(this.last_req.responseText); } // Convert a options object to an url query string. @@ -207,59 +213,44 @@ function CouchDB(name, options) { } function combine(object1, object2) { - if (!object2) { + if (!object2) return object1; - } - if (!object1) { + if (!object1) return object2; - } - for (var name in object2) { + + for (var name in object2) object1[name] = object2[name]; - } + return object1; } - function maybeThrowError(req) { - if (req.status >= 400) { - if (req.responseText) { - try { - var result = JSON.parse(req.responseText); - } catch (ParseError) { - var result = {error:"unknown", reason:req.responseText}; - } - } else { - var result = {}; - } - result.http_status = req.status; - throw result; - } - } + } +// this is the XMLHttpRequest object from last request made by the following +// CouchDB.* functions (except for calls to request itself). +// Use this from callers to check HTTP status or header values of requests. +CouchDB.last_req = null; + + CouchDB.allDbs = function() { - var req = CouchDB.request("GET", "/_all_dbs"); - var result = JSON.parse(req.responseText); - if (req.status != 200) - throw result; - return result; + CouchDB.last_req = CouchDB.request("GET", "/_all_dbs"); + CouchDB.maybeThrowError(CouchDB.last_req); + return JSON.parse(CouchDB.last_req.responseText); } CouchDB.getVersion = function() { - var req = CouchDB.request("GET", "/"); - var result = JSON.parse(req.responseText); - if (req.status != 200) - throw result; - return result.version; + CouchDB.last_req = CouchDB.request("GET", "/"); + CouchDB.maybeThrowError(CouchDB.last_req); + return JSON.parse(CouchDB.last_req.responseText).version; } CouchDB.replicate = function(source, target) { - var req = CouchDB.request("POST", "/_replicate", { + CouchDB.last_req = CouchDB.request("POST", "/_replicate", { body: JSON.stringify({source: source, target: target}) }); - var result = JSON.parse(req.responseText); - if (req.status != 200) - throw result; - return result; + CouchDB.maybeThrowError(CouchDB.last_req); + return JSON.parse(CouchDB.last_req.responseText); } CouchDB.request = function(method, uri, options) { @@ -272,7 +263,7 @@ CouchDB.request = function(method, uri, options) { } else { throw new Error("No XMLHTTPRequest support detected"); } - req.open(method, uri, false, options.username, options.password); + req.open(method, uri, false); if (options.headers) { var headers = options.headers; for (var headerName in headers) { @@ -297,12 +288,22 @@ CouchDB.newUuids = function(n) { } return uuids; } else { - var req = CouchDB.request("POST", "/_uuids?count=" + (100 + n)); - var result = JSON.parse(req.responseText); - if (req.status != 200) - throw result; + CouchDB.last_req = CouchDB.request("POST", "/_uuids?count=" + (100 + n)); + CouchDB.maybeThrowError(CouchDB.last_req); + var result = JSON.parse(CouchDB.last_req.responseText); CouchDB.uuids_cache = CouchDB.uuids_cache.concat(result.uuids.slice(0, 100)); return result.uuids.slice(100); } } + +CouchDB.maybeThrowError = function(req) { + if (req.status >= 400) { + try { + var result = JSON.parse(req.responseText); + } catch (ParseError) { + var result = {error:"unknown", reason:req.responseText}; + } + throw result; + } +} diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js index 8575da6f..3c14b761 100644 --- a/share/www/script/couch_tests.js +++ b/share/www/script/couch_tests.js @@ -1991,11 +1991,25 @@ var tests = { reason:"Documents must have an author field", http_status:403}; } - if (oldDoc && oldDoc.author != userCtx.name) { + + // Note, the next line could be: + // + // if (oldDoc && oldDoc.author != userCtx.name) { + // + // when name is the result of basic authentication, and added: + // + // headers: + // {"WWW-Authenticate": "Basic realm=\"" + userCtx.db + "\""}, + // + // to the thrown exception. But when trying to authenticate in this + // manner, most browsers have weird behaviors that make testing it + // in the browser difficult. So instead we use special header values + // a proof of concept. + if (oldDoc && oldDoc.author != userCtx["X-Couch-Username"]) { throw {error:"unauthorized", reason:"You are not the author of this document. You jerk.", headers: - {"WWW-Authenticate": "Basic realm=\"" + userCtx.db + "\""}, + {"X-Couch-Foo": "bar"}, http_status:401}; } }).toString() + ")" @@ -2003,14 +2017,14 @@ var tests = { db.save(designDoc); - var userDb = new CouchDB("test_suite_db", {username:"test user", password:"foo"}); + var userDb = new CouchDB("test_suite_db", {"X-Couch-Username":"test user"}); try { userDb.save({foo:1}); T(false && "Can't get here. Should have thrown an error"); } catch (e) { T(e.error == "forbidden"); - T(e.http_status == 403); + T(userDb.last_req.status == 403); } userDb.save({_id:"testdoc", foo:1, author:"test user"}); @@ -2019,7 +2033,7 @@ var tests = { doc.foo=2; userDb.save(doc); - var user2Db = new CouchDB("test_suite_db", {username:"test user2"}); + var user2Db = new CouchDB("test_suite_db", {"X-Couch-Username":"test user2"}); var doc = user2Db.open("testdoc"); doc.foo=3; @@ -2028,7 +2042,8 @@ var tests = { T(false && "Can't get here. Should have thrown an error 2"); } catch (e) { T(e.error == "unauthorized"); - T(e.http_status == 401); + T(user2Db.last_req.status == 401); + T(user2Db.last_req.getResponseHeader("X-Couch-Foo") == "bar"); } -- cgit v1.2.3