diff options
author | Damien F. Katz <damien@apache.org> | 2008-11-17 18:18:51 +0000 |
---|---|---|
committer | Damien F. Katz <damien@apache.org> | 2008-11-17 18:18:51 +0000 |
commit | aee6f18edf8cdf3f7c09c93fcf1af48c2c15fcd8 (patch) | |
tree | fd3a259ba4b0edd2c9ea5e6657f0ae314b36cbb5 | |
parent | d32d8acff4bac6f51b87ddef7091c04ff7245d40 (diff) |
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
-rw-r--r-- | etc/couchdb/local_dev.ini | 26 | ||||
-rw-r--r-- | share/www/script/couch.js | 181 | ||||
-rw-r--r-- | share/www/script/couch_tests.js | 27 | ||||
-rw-r--r-- | src/couchdb/couch_db.hrl | 3 | ||||
-rw-r--r-- | src/couchdb/couch_db_updater.erl | 2 | ||||
-rw-r--r-- | src/couchdb/couch_httpd.erl | 83 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_db.erl | 12 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_misc_handlers.erl | 20 | ||||
-rw-r--r-- | src/couchdb/couch_rep.erl | 89 | ||||
-rw-r--r-- | src/couchdb/couch_server.erl | 12 |
10 files changed, 271 insertions, 184 deletions
diff --git a/etc/couchdb/local_dev.ini b/etc/couchdb/local_dev.ini index 4081062b..47751df4 100644 --- a/etc/couchdb/local_dev.ini +++ b/etc/couchdb/local_dev.ini @@ -12,7 +12,7 @@ ;bind_address = 127.0.0.1 [log] -;level = info +level = debug [update_notification] ;unique notifier name=/full/path/to/exe -with "cmd line arg" @@ -24,3 +24,27 @@ foo = bar [test] foo = bar + +[test] +foo = bar + +[test] +foo = bar + +[test] +foo = bar + +[test] +foo = bar + +[test] +foo = bar + +[test] +foo = bar + +[test] +foo = bar + +[test] +foo = bar 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"); } diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl index 51f9b311..6769c840 100644 --- a/src/couchdb/couch_db.hrl +++ b/src/couchdb/couch_db.hrl @@ -59,7 +59,8 @@ {mochi_req, method, path_parts, - db_url_handlers + db_url_handlers, + user_ctx }). diff --git a/src/couchdb/couch_db_updater.erl b/src/couchdb/couch_db_updater.erl index b3df910f..fecc5a14 100644 --- a/src/couchdb/couch_db_updater.erl +++ b/src/couchdb/couch_db_updater.erl @@ -35,7 +35,7 @@ init({MainPid, DbName, Filepath, Fd, Options}) -> Db = init_db(DbName, Filepath, Fd, Header), Db2 = refresh_validate_doc_funs(Db), - {ok, Db#db{main_pid=MainPid}}. + {ok, Db2#db{main_pid=MainPid}}. terminate(_Reason, Db) -> close_db(Db). diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index c58255e3..6c8873dd 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -13,16 +13,17 @@ -module(couch_httpd). -include("couch_db.hrl"). --export([start_link/0, stop/0, handle_request/3]). +-export([start_link/0, stop/0, handle_request/4]). -export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1]). --export([check_is_admin/1,unquote/1,creds/1]). +-export([check_is_admin/1,unquote/1]). -export([parse_form/1,json_body/1,body/1,doc_etag/1]). -export([primary_header_value/2,partition/1,serve_file/3]). -export([start_chunked_response/3,send_chunk/2]). -export([start_json_response/2, start_json_response/3, end_json_response/1]). -export([send_response/4,send_method_not_allowed/2,send_error/4]). -export([send_json/2,send_json/3,send_json/4]). +-export([default_authenticate/1]). % Maximum size of document PUT request body (4GB) @@ -36,32 +37,24 @@ start_link() -> BindAddress = couch_config:get("httpd", "bind_address", any), Port = couch_config:get("httpd", "port", "5984"), - + AuthenticationFun = make_arity_1_fun( + couch_config:get("httpd", "authentication", + "{couch_httpd, default_authenticate}")), + UrlHandlersList = lists:map( fun({UrlKey, SpecStr}) -> - case couch_util:parse_term(SpecStr) of - {ok, {M, F, A}} -> - {list_to_binary(UrlKey), fun(Req) -> apply(M, F, [Req, A]) end}; - {ok, {M, F}} -> - {list_to_binary(UrlKey), fun(Req) -> apply(M, F, [Req]) end} - end + {list_to_binary(UrlKey), make_arity_1_fun(SpecStr)} end, couch_config:get("httpd_global_handlers")), DbUrlHandlersList = lists:map( fun({UrlKey, SpecStr}) -> - case couch_util:parse_term(SpecStr) of - {ok, {M, F, A}} -> - {list_to_binary(UrlKey), - fun(Req, Db) -> apply(M, F, [Req, Db, A]) end}; - {ok, {M, F}} -> - {list_to_binary(UrlKey), - fun(Req, Db) -> apply(M, F, [Req, Db]) end} - end + {list_to_binary(UrlKey), make_arity_2_fun(SpecStr)} end, couch_config:get("httpd_db_handlers")), UrlHandlers = dict:from_list(UrlHandlersList), DbUrlHandlers = dict:from_list(DbUrlHandlersList), Loop = fun(Req)-> - apply(?MODULE, handle_request, [Req, UrlHandlers, DbUrlHandlers]) + apply(?MODULE, handle_request, + [Req, UrlHandlers, DbUrlHandlers, AuthenticationFun]) end, % and off we go @@ -76,6 +69,8 @@ start_link() -> ?MODULE:stop(); ("httpd", "port") -> ?MODULE:stop(); + ("httpd", "authentication") -> + ?MODULE:stop(); ("httpd_global_handlers", _) -> ?MODULE:stop(); ("httpd_db_handlers", _) -> @@ -84,11 +79,29 @@ start_link() -> {ok, Pid}. +% SpecStr is a string like "{my_module, my_fun}" or "{my_module, my_fun, foo}" +make_arity_1_fun(SpecStr) -> + case couch_util:parse_term(SpecStr) of + {ok, {Mod, Fun, SpecArg}} -> + fun(Arg) -> apply(Mod, Fun, [Arg, SpecArg]) end; + {ok, {Mod, Fun}} -> + fun(Arg) -> apply(Mod, Fun, [Arg]) end + end. + +make_arity_2_fun(SpecStr) -> + case couch_util:parse_term(SpecStr) of + {ok, {Mod, Fun, SpecArg}} -> + fun(Arg1, Arg2) -> apply(Mod, Fun, [Arg1, Arg2, SpecArg]) end; + {ok, {Mod, Fun}} -> + fun(Arg1, Arg2) -> apply(Mod, Fun, [Arg1, Arg2]) end + end. + + stop() -> mochiweb_http:stop(?MODULE). -handle_request(MochiReq, UrlHandlers, DbUrlHandlers) -> +handle_request(MochiReq, UrlHandlers, DbUrlHandlers, AuthenticationFun) -> % for the path, use the raw path with the query string and fragment % removed, but URL quoting left intact @@ -128,13 +141,16 @@ handle_request(MochiReq, UrlHandlers, DbUrlHandlers) -> || Part <- string:tokens(Path, "/")], db_url_handlers = DbUrlHandlers }, - DefaultFun = fun couch_httpd_db:handle_request/1, HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun), + CouchHeaders = [{?l2b(K), ?l2b(V)} + || {"X-Couch-" ++ _= K,V} + <- mochiweb_headers:to_list(MochiReq:get(headers))], {ok, Resp} = try - HandlerFun(HttpReq) + {UserCtxProps} = AuthenticationFun(HttpReq), + HandlerFun(HttpReq#httpd{user_ctx={UserCtxProps ++ CouchHeaders}}) catch Error -> send_error(HttpReq, Error) @@ -150,6 +166,17 @@ handle_request(MochiReq, UrlHandlers, DbUrlHandlers) -> +default_authenticate(Req) -> + % by default, we just assume the users credentials for basic authentication + % are correct. + case basic_username_pw(Req) of + {Username, _Pw} -> + {[{<<"name">>, ?l2b(Username)}]}; + nil -> + {[]} + end. + + % Utilities partition(Path) -> @@ -197,17 +224,7 @@ json_body(#httpd{mochi_req=MochiReq}) -> doc_etag(#doc{revs=[DiskRev|_]}) -> "\"" ++ binary_to_list(DiskRev) ++ "\"". - -% user credentials -creds(Req) -> - case username_pw(Req) of - {User, _Pw} -> - {[{<<"name">>, ?l2b(User)}]}; - nil -> - {[]} - end. - -username_pw(Req) -> +basic_username_pw(Req) -> case header_value(Req, "Authorization") of "Basic " ++ Base64Value -> case string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":") of @@ -224,7 +241,7 @@ username_pw(Req) -> check_is_admin(Req) -> IsNamedAdmin = - case username_pw(Req) of + case basic_username_pw(Req) of {User, Pass} -> couch_server:is_admin(User, Pass); nil -> diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index bc8e2019..a239ceb5 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -41,9 +41,9 @@ handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, do_db_req(Req, Handler) end. -create_db_req(Req, DbName) -> +create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> ok = couch_httpd:check_is_admin(Req), - case couch_server:create(DbName, [{creds, couch_httpd:creds(Req)}]) of + case couch_server:create(DbName, [{user_ctx, UserCtx}]) of {ok, Db} -> couch_db:close(Db), send_json(Req, 201, {[{ok, true}]}); @@ -51,17 +51,17 @@ create_db_req(Req, DbName) -> throw(Error) end. -delete_db_req(Req, DbName) -> +delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> ok = couch_httpd:check_is_admin(Req), - case couch_server:delete(DbName, [{creds, couch_httpd:creds(Req)}]) of + case couch_server:delete(DbName, [{user_ctx, UserCtx}]) of ok -> send_json(Req, 200, {[{ok, true}]}); Error -> throw(Error) end. -do_db_req(#httpd{path_parts=[DbName|_]}=Req, Fun) -> - case couch_db:open(DbName, [{creds, couch_httpd:creds(Req)}]) of +do_db_req(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Fun) -> + case couch_db:open(DbName, [{user_ctx, UserCtx}]) of {ok, Db} -> try Fun(Req, Db) diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl index bcfe17c4..b62a4b85 100644 --- a/src/couchdb/couch_httpd_misc_handlers.erl +++ b/src/couchdb/couch_httpd_misc_handlers.erl @@ -62,12 +62,28 @@ handle_all_dbs_req(Req) -> send_method_not_allowed(Req, "GET,HEAD"). -handle_replicate_req(#httpd{method='POST'}=Req) -> +handle_replicate_req(#httpd{user_ctx=UserCtx,method='POST'}=Req) -> {Props} = couch_httpd:json_body(Req), Source = proplists:get_value(<<"source">>, Props), Target = proplists:get_value(<<"target">>, Props), + + {SrcOpts} = proplists:get_value(<<"source_options">>, Props, {[]}), + {SrcHeadersBinary} = proplists:get_value(<<"headers">>, SrcOpts, {[]}), + SrcHeaders = [{?b2l(K),(V)} || {K,V} <- SrcHeadersBinary], + + {TgtOpts} = proplists:get_value(<<"target_options">>, Props, {[]}), + {TgtHeadersBinary} = proplists:get_value(<<"headers">>, TgtOpts, {[]}), + TgtHeaders = [{?b2l(K),(V)} || {K,V} <- TgtHeadersBinary], + {Options} = proplists:get_value(<<"options">>, Props, {[]}), - {ok, {JsonResults}} = couch_rep:replicate(Source, Target, Options), + Options2 = [{source_options, + [{headers, SrcHeaders}, + {user_ctx, UserCtx}]}, + {target_options, + [{headers, TgtHeaders}, + {user_ctx, UserCtx}]} + | Options], + {ok, {JsonResults}} = couch_rep:replicate(Source, Target, Options2), send_json(Req, {[{ok, true} | JsonResults]}); handle_replicate_req(Req) -> send_method_not_allowed(Req, "POST"). diff --git a/src/couchdb/couch_rep.erl b/src/couchdb/couch_rep.erl index d0a12fc7..eda62c86 100644 --- a/src/couchdb/couch_rep.erl +++ b/src/couchdb/couch_rep.erl @@ -14,6 +14,11 @@ -include("couch_db.hrl"). +-record(http_db, { + uri, + headers +}). + -export([replicate/2, replicate/3]). url_encode(Bin) when is_binary(Bin) -> @@ -44,9 +49,11 @@ replicate(DbNameA, DbNameB) -> replicate(DbNameA, DbNameB, []). replicate(Source, Target, Options) -> - {ok, DbSrc} = open_db(Source), + {ok, DbSrc} = open_db(Source, + proplists:get_value(source_options, Options, [])), try - {ok, DbTgt} = open_db(Target), + {ok, DbTgt} = open_db(Target, + proplists:get_value(target_options, Options, [])), try replicate2(Source, DbSrc, Target, DbTgt, Options) after @@ -216,20 +223,19 @@ save_docs_loop(DbTarget, DocsWritten) -> end. -do_http_request(Url, Action) -> - do_http_request(Url, Action, []). +do_http_request(Url, Action, Headers) -> + do_http_request(Url, Action, Headers, []). -do_http_request(Url, Action, JsonBody) -> +do_http_request(Url, Action, Headers, JsonBody) -> ?LOG_DEBUG("couch_rep HTTP client request:", []), ?LOG_DEBUG("\tAction: ~p", [Action]), ?LOG_DEBUG("\tUrl: ~p", [Url]), - Request = case JsonBody of [] -> - {Url, []}; + {Url, Headers}; _ -> - {Url, [], "application/json; charset=utf-8", iolist_to_binary(?JSON_ENCODE(JsonBody))} + {Url, Headers, "application/json; charset=utf-8", iolist_to_binary(?JSON_ENCODE(JsonBody))} end, {ok, {{_, ResponseCode,_},_Headers, ResponseBody}} = http:request(Action, Request, [], []), if @@ -249,29 +255,32 @@ fix_url(UrlBin) -> Url = binary_to_list(UrlBin), case lists:last(Url) of $/ -> - {ok, Url}; + Url; _ -> - {ok, Url ++ "/"} + Url ++ "/" end. -open_db(<<"http://", _/binary>>=UrlBin)-> - fix_url(UrlBin); -open_db(<<"https://", _/binary>>=UrlBin)-> - fix_url(UrlBin); -open_db(DbName)-> - couch_db:open(DbName, []). - -close_db("http://" ++ _)-> - ok; -close_db("https://" ++ _)-> +open_http_db(UrlBin, Options) -> + Headers = proplists:get_value(headers, Options, {[]}), + {ok, #http_db{uri=fix_url(UrlBin), headers=Headers}}. + +open_db(<<"http://", _/binary>>=Url, Options)-> + open_http_db(Url, Options); +open_db(<<"https://", _/binary>>=Url, Options)-> + open_http_db(Url, Options); +open_db(DbName, Options)-> + couch_db:open(DbName, Options). + +close_db(#http_db{})-> ok; -close_db(DbName)-> - couch_db:close(DbName). +close_db(Db)-> + couch_db:close(Db). -enum_docs_since(DbUrl, StartSeq, InFun, InAcc) when is_list(DbUrl) -> - Url = DbUrl ++ "_all_docs_by_seq?count=100&startkey=" ++ integer_to_list(StartSeq), - {Results} = do_http_request(Url, get), +enum_docs_since(#http_db{uri=DbUrl, headers=Headers}=Db, Start, InFun, InAcc)-> + Url = DbUrl ++ "_all_docs_by_seq?count=100&startkey=" + ++ integer_to_list(Start), + {Results} = do_http_request(Url, get, Headers), DocInfoList= lists:map(fun({RowInfoList}) -> {RowValueProps} = proplists:get_value(<<"value">>, RowInfoList), @@ -291,23 +300,25 @@ enum_docs_since(DbUrl, StartSeq, InFun, InAcc) when is_list(DbUrl) -> _ -> Acc2 = enum_docs0(InFun, DocInfoList, InAcc), #doc_info{update_seq=LastSeq} = lists:last(DocInfoList), - enum_docs_since(DbUrl, LastSeq, InFun, Acc2) + enum_docs_since(Db, LastSeq, InFun, Acc2) end; enum_docs_since(DbSource, StartSeq, Fun, Acc) -> couch_db:enum_docs_since(DbSource, StartSeq, Fun, Acc). -get_missing_revs(DbUrl, DocIdRevsList) when is_list(DbUrl) -> - {ResponseMembers} = do_http_request(DbUrl ++ "_missing_revs", post, {DocIdRevsList}), +get_missing_revs(#http_db{uri=DbUrl, headers=Headers}, DocIdRevsList) -> + {ResponseMembers} = do_http_request(DbUrl ++ "_missing_revs", post, Headers, + {DocIdRevsList}), {DocMissingRevsList} = proplists:get_value(<<"missing_revs">>, ResponseMembers), {ok, DocMissingRevsList}; get_missing_revs(Db, DocId) -> couch_db:get_missing_revs(Db, DocId). -update_doc(DbUrl, #doc{id=DocId}=Doc, _Options) when is_list(DbUrl) -> +update_doc(#http_db{uri=DbUrl, headers=Headers}, #doc{id=DocId}=Doc, Options) -> + [] = Options, Url = DbUrl ++ url_encode(DocId), - {ResponseMembers} = - do_http_request(Url, put, couch_doc:to_json_obj(Doc, [revs,attachments])), + {ResponseMembers} = do_http_request(Url, put, Headers, + couch_doc:to_json_obj(Doc, [revs,attachments])), RevId = proplists:get_value(<<"_rev">>, ResponseMembers), {ok, RevId}; update_doc(Db, Doc, Options) -> @@ -315,28 +326,30 @@ update_doc(Db, Doc, Options) -> update_docs(_, [], _, _) -> ok; -update_docs(DbUrl, Docs, [], NewEdits) when is_list(DbUrl) -> +update_docs(#http_db{uri=DbUrl, headers=Headers}, Docs, [], NewEdits) -> JsonDocs = [couch_doc:to_json_obj(Doc, [revs,attachments]) || Doc <- Docs], {Returned} = - do_http_request(DbUrl ++ "_bulk_docs", post, {[{new_edits, NewEdits}, {docs, JsonDocs}]}), + do_http_request(DbUrl ++ "_bulk_docs", post, Headers, + {[{new_edits, NewEdits}, {docs, JsonDocs}]}), true = proplists:get_value(<<"ok">>, Returned), ok; update_docs(Db, Docs, Options, NewEdits) -> couch_db:update_docs(Db, Docs, Options, NewEdits). -open_doc(DbUrl, DocId, []) when is_list(DbUrl) -> - case do_http_request(DbUrl ++ url_encode(DocId), get) of +open_doc(#http_db{uri=DbUrl, headers=Headers}, DocId, Options) -> + [] = Options, + case do_http_request(DbUrl ++ url_encode(DocId), get, Headers) of {[{<<"error">>, ErrId}, {<<"reason">>, Reason}]} -> % binaries? {list_to_atom(binary_to_list(ErrId)), Reason}; Doc -> {ok, couch_doc:from_json_obj(Doc)} end; -open_doc(Db, DocId, Options) when not is_list(Db) -> +open_doc(Db, DocId, Options) -> couch_db:open_doc(Db, DocId, Options). -open_doc_revs(DbUrl, DocId, Revs, Options) when is_list(DbUrl) -> +open_doc_revs(#http_db{uri=DbUrl, headers=Headers}, DocId, Revs, Options) -> QueryOptionStrs = lists:map(fun(latest) -> % latest is only option right now @@ -344,7 +357,7 @@ open_doc_revs(DbUrl, DocId, Revs, Options) when is_list(DbUrl) -> end, Options), RevsQueryStrs = lists:flatten(?JSON_ENCODE(Revs)), Url = DbUrl ++ url_encode(DocId) ++ "?" ++ couch_util:implode(["revs=true", "attachments=true", "open_revs=" ++ RevsQueryStrs ] ++ QueryOptionStrs, "&"), - JsonResults = do_http_request(Url, get, []), + JsonResults = do_http_request(Url, get, Headers), Results = lists:map( fun({[{<<"missing">>, Rev}]}) -> diff --git a/src/couchdb/couch_server.erl b/src/couchdb/couch_server.erl index b0d03a53..a81ca5e1 100644 --- a/src/couchdb/couch_server.erl +++ b/src/couchdb/couch_server.erl @@ -202,7 +202,7 @@ handle_call(get_root, _From, #server{root_dir=Root}=Server) -> {reply, {ok, Root}, Server}; handle_call({open, DbName, Options}, {FromPid,_}, Server) -> DbNameList = binary_to_list(DbName), - UserCreds = proplists:get_value(creds, Options, nil), + UserCtx = proplists:get_value(user_ctx, Options, nil), case check_dbname(Server, DbNameList) of ok -> Filepath = get_full_filename(Server, DbNameList), @@ -218,7 +218,7 @@ handle_call({open, DbName, Options}, {FromPid,_}, Server) -> true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}), DbsOpen = Server2#server.current_dbs_open + 1, {reply, - couch_db:open_ref_counted(MainPid, FromPid, UserCreds), + couch_db:open_ref_counted(MainPid, FromPid, UserCtx), Server2#server{current_dbs_open=DbsOpen}}; Error -> {reply, Error, Server2} @@ -231,7 +231,7 @@ handle_call({open, DbName, Options}, {FromPid,_}, Server) -> true = ets:delete(couch_dbs_by_lru, PrevLruTime), true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}), {reply, - couch_db:open_ref_counted(MainPid, FromPid, UserCreds), + couch_db:open_ref_counted(MainPid, FromPid, UserCtx), Server} end; Error -> @@ -239,7 +239,7 @@ handle_call({open, DbName, Options}, {FromPid,_}, Server) -> end; handle_call({create, DbName, Options}, {FromPid,_}, Server) -> DbNameList = binary_to_list(DbName), - UserCreds = proplists:get_value(creds, Options, nil), + UserCtx = proplists:get_value(user_ctx, Options, nil), case check_dbname(Server, DbNameList) of ok -> Filepath = get_full_filename(Server, DbNameList), @@ -255,7 +255,7 @@ handle_call({create, DbName, Options}, {FromPid,_}, Server) -> DbsOpen = Server#server.current_dbs_open + 1, couch_db_update_notifier:notify({created, DbName}), {reply, - couch_db:open_ref_counted(MainPid, FromPid, UserCreds), + couch_db:open_ref_counted(MainPid, FromPid, UserCtx), Server#server{current_dbs_open=DbsOpen}}; Error -> {reply, Error, Server} @@ -268,7 +268,7 @@ handle_call({create, DbName, Options}, {FromPid,_}, Server) -> end; handle_call({delete, DbName, Options}, _From, Server) -> DbNameList = binary_to_list(DbName), - _UserCreds = proplists:get_value(creds, Options, nil), + _UserCtx = proplists:get_value(user_ctx, Options, nil), case check_dbname(Server, DbNameList) of ok -> FullFilepath = get_full_filename(Server, DbNameList), |