summaryrefslogtreecommitdiff
path: root/src/couchdb
diff options
context:
space:
mode:
authorDamien F. Katz <damien@apache.org>2008-11-17 18:18:51 +0000
committerDamien F. Katz <damien@apache.org>2008-11-17 18:18:51 +0000
commitaee6f18edf8cdf3f7c09c93fcf1af48c2c15fcd8 (patch)
treefd3a259ba4b0edd2c9ea5e6657f0ae314b36cbb5 /src/couchdb
parentd32d8acff4bac6f51b87ddef7091c04ff7245d40 (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
Diffstat (limited to 'src/couchdb')
-rw-r--r--src/couchdb/couch_db.hrl3
-rw-r--r--src/couchdb/couch_db_updater.erl2
-rw-r--r--src/couchdb/couch_httpd.erl83
-rw-r--r--src/couchdb/couch_httpd_db.erl12
-rw-r--r--src/couchdb/couch_httpd_misc_handlers.erl20
-rw-r--r--src/couchdb/couch_rep.erl89
-rw-r--r--src/couchdb/couch_server.erl12
7 files changed, 134 insertions, 87 deletions
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),