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 --- src/couchdb/couch_db.hrl | 3 +- src/couchdb/couch_db_updater.erl | 2 +- src/couchdb/couch_httpd.erl | 83 ++++++++++++++++------------ src/couchdb/couch_httpd_db.erl | 12 ++--- src/couchdb/couch_httpd_misc_handlers.erl | 20 ++++++- src/couchdb/couch_rep.erl | 89 ++++++++++++++++++------------- src/couchdb/couch_server.erl | 12 ++--- 7 files changed, 134 insertions(+), 87 deletions(-) (limited to 'src') 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), -- cgit v1.2.3