Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id A90E21078E for ; Sat, 11 Jan 2014 14:00:29 +0000 (UTC) Received: (qmail 44969 invoked by uid 500); 11 Jan 2014 14:00:27 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 44824 invoked by uid 500); 11 Jan 2014 14:00:23 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 44253 invoked by uid 99); 11 Jan 2014 14:00:15 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Sat, 11 Jan 2014 14:00:15 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 3D6E482D39E; Sat, 11 Jan 2014 14:00:14 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: benoitc@apache.org To: commits@couchdb.apache.org Date: Sat, 11 Jan 2014 14:00:18 -0000 Message-Id: <3f48329e402e4239aba902723bbf6a1f@git.apache.org> In-Reply-To: <0c75977b46b444ef99f108ebf16a1538@git.apache.org> References: <0c75977b46b444ef99f108ebf16a1538@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [6/7] make couch_httpd a full couch application http://git-wip-us.apache.org/repos/asf/couchdb/blob/5618e610/src/apps/couch/src/couch_httpd_db.erl ---------------------------------------------------------------------- diff --git a/src/apps/couch/src/couch_httpd_db.erl b/src/apps/couch/src/couch_httpd_db.erl deleted file mode 100644 index 0a7c17c..0000000 --- a/src/apps/couch/src/couch_httpd_db.erl +++ /dev/null @@ -1,1226 +0,0 @@ -% Licensed under the Apache License, Version 2.0 (the "License"); you may not -% use this file except in compliance with the License. You may obtain a copy of -% the License at -% -% http://www.apache.org/licenses/LICENSE-2.0 -% -% Unless required by applicable law or agreed to in writing, software -% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -% License for the specific language governing permissions and limitations under -% the License. - --module(couch_httpd_db). --include("couch_db.hrl"). - --export([handle_request/1, handle_compact_req/2, handle_design_req/2, - db_req/2, couch_doc_open/4,handle_changes_req/2, - update_doc_result_to_json/1, update_doc_result_to_json/2, - handle_design_info_req/3]). - --import(couch_httpd, - [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, - start_json_response/2,send_chunk/2,last_chunk/1,end_json_response/1, - start_chunked_response/3, absolute_uri/2, send/2, - start_response_length/4, send_error/4]). - --record(doc_query_args, { - options = [], - rev = nil, - open_revs = [], - update_type = interactive_edit, - atts_since = nil -}). - -% Database request handlers -handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, - db_url_handlers=DbUrlHandlers}=Req)-> - case {Method, RestParts} of - {'PUT', []} -> - create_db_req(Req, DbName); - {'DELETE', []} -> - % if we get ?rev=... the user is using a faulty script where the - % document id is empty by accident. Let them recover safely. - case couch_httpd:qs_value(Req, "rev", false) of - false -> delete_db_req(Req, DbName); - _Rev -> throw({bad_request, - "You tried to DELETE a database with a ?=rev parameter. " - ++ "Did you mean to DELETE a document instead?"}) - end; - {_, []} -> - do_db_req(Req, fun db_req/2); - {_, [SecondPart|_]} -> - Handler = couch_util:dict_find(SecondPart, DbUrlHandlers, fun db_req/2), - do_db_req(Req, Handler) - end. - -handle_changes_req(#httpd{method='POST'}=Req, Db) -> - couch_httpd:validate_ctype(Req, "application/json"), - handle_changes_req1(Req, Db); -handle_changes_req(#httpd{method='GET'}=Req, Db) -> - handle_changes_req1(Req, Db); -handle_changes_req(#httpd{path_parts=[_,<<"_changes">>]}=Req, _Db) -> - send_method_not_allowed(Req, "GET,HEAD,POST"). - -handle_changes_req1(Req, #db{name=DbName}=Db) -> - AuthDbName = ?l2b(couch_config:get("couch_httpd_auth", "authentication_db")), - case AuthDbName of - DbName -> - % in the authentication database, _changes is admin-only. - ok = couch_db:check_is_admin(Db); - _Else -> - % on other databases, _changes is free for all. - ok - end, - handle_changes_req2(Req, Db). - -handle_changes_req2(Req, Db) -> - MakeCallback = fun(Resp) -> - fun({change, {ChangeProp}=Change, _}, "eventsource") -> - Seq = proplists:get_value(<<"seq">>, ChangeProp), - send_chunk(Resp, ["data: ", ?JSON_ENCODE(Change), - "\n", "id: ", ?JSON_ENCODE(Seq), - "\n\n"]); - ({change, Change, _}, "continuous") -> - send_chunk(Resp, [?JSON_ENCODE(Change) | "\n"]); - ({change, Change, Prepend}, _) -> - send_chunk(Resp, [Prepend, ?JSON_ENCODE(Change)]); - (start, "eventsource") -> - ok; - (start, "continuous") -> - ok; - (start, _) -> - send_chunk(Resp, "{\"results\":[\n"); - ({stop, _EndSeq}, "eventsource") -> - end_json_response(Resp); - ({stop, EndSeq}, "continuous") -> - send_chunk( - Resp, - [?JSON_ENCODE({[{<<"last_seq">>, EndSeq}]}) | "\n"] - ), - end_json_response(Resp); - ({stop, EndSeq}, _) -> - send_chunk( - Resp, - io_lib:format("\n],\n\"last_seq\":~w}\n", [EndSeq]) - ), - end_json_response(Resp); - (timeout, _) -> - send_chunk(Resp, "\n") - end - end, - ChangesArgs = parse_changes_query(Req, Db), - ChangesFun = couch_changes:handle_changes(ChangesArgs, Req, Db), - WrapperFun = case ChangesArgs#changes_args.feed of - "normal" -> - {ok, Info} = couch_db:get_db_info(Db), - CurrentEtag = couch_httpd:make_etag(Info), - fun(FeedChangesFun) -> - couch_httpd:etag_respond( - Req, - CurrentEtag, - fun() -> - {ok, Resp} = couch_httpd:start_json_response( - Req, 200, [{"ETag", CurrentEtag}] - ), - FeedChangesFun(MakeCallback(Resp)) - end - ) - end; - "eventsource" -> - Headers = [ - {"Content-Type", "text/event-stream"}, - {"Cache-Control", "no-cache"} - ], - {ok, Resp} = couch_httpd:start_chunked_response(Req, 200, Headers), - fun(FeedChangesFun) -> - FeedChangesFun(MakeCallback(Resp)) - end; - _ -> - % "longpoll" or "continuous" - {ok, Resp} = couch_httpd:start_json_response(Req, 200), - fun(FeedChangesFun) -> - FeedChangesFun(MakeCallback(Resp)) - end - end, - couch_stats_collector:increment( - {httpd, clients_requesting_changes} - ), - try - WrapperFun(ChangesFun) - after - couch_stats_collector:decrement( - {httpd, clients_requesting_changes} - ) - end. - -handle_compact_req(#httpd{method='POST'}=Req, Db) -> - case Req#httpd.path_parts of - [_DbName, <<"_compact">>] -> - ok = couch_db:check_is_admin(Db), - couch_httpd:validate_ctype(Req, "application/json"), - {ok, _} = couch_db:start_compact(Db), - send_json(Req, 202, {[{ok, true}]}); - [_DbName, <<"_compact">>, DesignName | _] -> - DesignId = <<"_design/", DesignName/binary>>, - DDoc = couch_httpd_db:couch_doc_open( - Db, DesignId, nil, [ejson_body] - ), - couch_mrview_http:handle_compact_req(Req, Db, DDoc) - end; - -handle_compact_req(Req, _Db) -> - send_method_not_allowed(Req, "POST"). - - -handle_design_req(#httpd{ - path_parts=[_DbName, _Design, DesignName, <<"_",_/binary>> = Action | _Rest], - design_url_handlers = DesignUrlHandlers - }=Req, Db) -> - case couch_db:is_system_db(Db) of - true -> - case (catch couch_db:check_is_admin(Db)) of - ok -> ok; - _ -> - throw({forbidden, <<"Only admins can access design document", - " actions for system databases.">>}) - end; - false -> ok - end, - - % load ddoc - DesignId = <<"_design/", DesignName/binary>>, - DDoc = couch_httpd_db:couch_doc_open(Db, DesignId, nil, [ejson_body]), - Handler = couch_util:dict_find(Action, DesignUrlHandlers, fun(_, _, _) -> - throw({not_found, <<"missing handler: ", Action/binary>>}) - end), - Handler(Req, Db, DDoc); - -handle_design_req(Req, Db) -> - db_req(Req, Db). - -handle_design_info_req(#httpd{ - method='GET', - path_parts=[_DbName, _Design, DesignName, _] - }=Req, Db, _DDoc) -> - DesignId = <<"_design/", DesignName/binary>>, - DDoc = couch_httpd_db:couch_doc_open(Db, DesignId, nil, [ejson_body]), - couch_mrview_http:handle_info_req(Req, Db, DDoc). - -create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> - ok = couch_httpd:verify_is_server_admin(Req), - case couch_server:create(DbName, [{user_ctx, UserCtx}]) of - {ok, Db} -> - couch_db:close(Db), - DbUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)), - send_json(Req, 201, [{"Location", DbUrl}], {[{ok, true}]}); - Error -> - throw(Error) - end. - -delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> - ok = couch_httpd:verify_is_server_admin(Req), - case couch_server:delete(DbName, [{user_ctx, UserCtx}]) of - ok -> - send_json(Req, 200, {[{ok, true}]}); - Error -> - throw(Error) - end. - -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) - after - catch couch_db:close(Db) - end; - Error -> - throw(Error) - end. - -db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) -> - {ok, DbInfo} = couch_db:get_db_info(Db), - send_json(Req, {DbInfo}); - -db_req(#httpd{method='POST',path_parts=[_DbName]}=Req, Db) -> - couch_httpd:validate_ctype(Req, "application/json"), - Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)), - validate_attachment_names(Doc), - Doc2 = case Doc#doc.id of - <<"">> -> - Doc#doc{id=couch_uuids:new(), revs={0, []}}; - _ -> - Doc - end, - DocId = Doc2#doc.id, - update_doc(Req, Db, DocId, Doc2); - -db_req(#httpd{path_parts=[_DbName]}=Req, _Db) -> - send_method_not_allowed(Req, "DELETE,GET,HEAD,POST"); - -db_req(#httpd{method='POST',path_parts=[_,<<"_ensure_full_commit">>]}=Req, Db) -> - couch_httpd:validate_ctype(Req, "application/json"), - UpdateSeq = couch_db:get_update_seq(Db), - CommittedSeq = couch_db:get_committed_update_seq(Db), - {ok, StartTime} = - case couch_httpd:qs_value(Req, "seq") of - undefined -> - couch_db:ensure_full_commit(Db); - RequiredStr -> - RequiredSeq = list_to_integer(RequiredStr), - if RequiredSeq > UpdateSeq -> - throw({bad_request, - "can't do a full commit ahead of current update_seq"}); - RequiredSeq > CommittedSeq -> - couch_db:ensure_full_commit(Db); - true -> - {ok, Db#db.instance_start_time} - end - end, - send_json(Req, 201, {[ - {ok, true}, - {instance_start_time, StartTime} - ]}); - -db_req(#httpd{path_parts=[_,<<"_ensure_full_commit">>]}=Req, _Db) -> - send_method_not_allowed(Req, "POST"); - -db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) -> - couch_stats_collector:increment({httpd, bulk_requests}), - couch_httpd:validate_ctype(Req, "application/json"), - {JsonProps} = couch_httpd:json_body_obj(Req), - case couch_util:get_value(<<"docs">>, JsonProps) of - undefined -> - send_error(Req, 400, <<"bad_request">>, <<"Missing JSON list of 'docs'">>); - DocsArray -> - case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of - "true" -> - Options = [full_commit]; - "false" -> - Options = [delay_commit]; - _ -> - Options = [] - end, - case couch_util:get_value(<<"new_edits">>, JsonProps, true) of - true -> - Docs = lists:map( - fun({ObjProps} = JsonObj) -> - Doc = couch_doc:from_json_obj(JsonObj), - validate_attachment_names(Doc), - Id = case Doc#doc.id of - <<>> -> couch_uuids:new(); - Id0 -> Id0 - end, - case couch_util:get_value(<<"_rev">>, ObjProps) of - undefined -> - Revs = {0, []}; - Rev -> - {Pos, RevId} = couch_doc:parse_rev(Rev), - Revs = {Pos, [RevId]} - end, - Doc#doc{id=Id,revs=Revs} - end, - DocsArray), - Options2 = - case couch_util:get_value(<<"all_or_nothing">>, JsonProps) of - true -> [all_or_nothing|Options]; - _ -> Options - end, - case couch_db:update_docs(Db, Docs, Options2) of - {ok, Results} -> - % output the results - DocResults = lists:zipwith(fun update_doc_result_to_json/2, - Docs, Results), - send_json(Req, 201, DocResults); - {aborted, Errors} -> - ErrorsJson = - lists:map(fun update_doc_result_to_json/1, Errors), - send_json(Req, 417, ErrorsJson) - end; - false -> - Docs = lists:map(fun(JsonObj) -> - Doc = couch_doc:from_json_obj(JsonObj), - validate_attachment_names(Doc), - Doc - end, DocsArray), - {ok, Errors} = couch_db:update_docs(Db, Docs, Options, replicated_changes), - ErrorsJson = - lists:map(fun update_doc_result_to_json/1, Errors), - send_json(Req, 201, ErrorsJson) - end - end; -db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) -> - send_method_not_allowed(Req, "POST"); - -db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) -> - couch_httpd:validate_ctype(Req, "application/json"), - {IdsRevs} = couch_httpd:json_body_obj(Req), - IdsRevs2 = [{Id, couch_doc:parse_revs(Revs)} || {Id, Revs} <- IdsRevs], - - case couch_db:purge_docs(Db, IdsRevs2) of - {ok, PurgeSeq, PurgedIdsRevs} -> - PurgedIdsRevs2 = [{Id, couch_doc:revs_to_strs(Revs)} || {Id, Revs} <- PurgedIdsRevs], - send_json(Req, 200, {[{<<"purge_seq">>, PurgeSeq}, {<<"purged">>, {PurgedIdsRevs2}}]}); - Error -> - throw(Error) - end; - -db_req(#httpd{path_parts=[_,<<"_purge">>]}=Req, _Db) -> - send_method_not_allowed(Req, "POST"); - -db_req(#httpd{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) -> - {JsonDocIdRevs} = couch_httpd:json_body_obj(Req), - JsonDocIdRevs2 = [{Id, [couch_doc:parse_rev(RevStr) || RevStr <- RevStrs]} || {Id, RevStrs} <- JsonDocIdRevs], - {ok, Results} = couch_db:get_missing_revs(Db, JsonDocIdRevs2), - Results2 = [{Id, couch_doc:revs_to_strs(Revs)} || {Id, Revs, _} <- Results], - send_json(Req, {[ - {missing_revs, {Results2}} - ]}); - -db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) -> - send_method_not_allowed(Req, "POST"); - -db_req(#httpd{method='POST',path_parts=[_,<<"_revs_diff">>]}=Req, Db) -> - {JsonDocIdRevs} = couch_httpd:json_body_obj(Req), - JsonDocIdRevs2 = - [{Id, couch_doc:parse_revs(RevStrs)} || {Id, RevStrs} <- JsonDocIdRevs], - {ok, Results} = couch_db:get_missing_revs(Db, JsonDocIdRevs2), - Results2 = - lists:map(fun({Id, MissingRevs, PossibleAncestors}) -> - {Id, - {[{missing, couch_doc:revs_to_strs(MissingRevs)}] ++ - if PossibleAncestors == [] -> - []; - true -> - [{possible_ancestors, - couch_doc:revs_to_strs(PossibleAncestors)}] - end}} - end, Results), - send_json(Req, {Results2}); - -db_req(#httpd{path_parts=[_,<<"_revs_diff">>]}=Req, _Db) -> - send_method_not_allowed(Req, "POST"); - -db_req(#httpd{method='PUT',path_parts=[_,<<"_security">>]}=Req, Db) -> - SecObj = couch_httpd:json_body(Req), - ok = couch_db:set_security(Db, SecObj), - send_json(Req, {[{<<"ok">>, true}]}); - -db_req(#httpd{method='GET',path_parts=[_,<<"_security">>]}=Req, Db) -> - send_json(Req, couch_db:get_security(Db)); - -db_req(#httpd{path_parts=[_,<<"_security">>]}=Req, _Db) -> - send_method_not_allowed(Req, "PUT,GET"); - -db_req(#httpd{method='PUT',path_parts=[_,<<"_revs_limit">>]}=Req, - Db) -> - Limit = couch_httpd:json_body(Req), - case is_integer(Limit) of - true -> - ok = couch_db:set_revs_limit(Db, Limit), - send_json(Req, {[{<<"ok">>, true}]}); - false -> - throw({bad_request, <<"Rev limit has to be an integer">>}) - end; - -db_req(#httpd{method='GET',path_parts=[_,<<"_revs_limit">>]}=Req, Db) -> - send_json(Req, couch_db:get_revs_limit(Db)); - -db_req(#httpd{path_parts=[_,<<"_revs_limit">>]}=Req, _Db) -> - send_method_not_allowed(Req, "PUT,GET"); - -% Special case to enable using an unencoded slash in the URL of design docs, -% as slashes in document IDs must otherwise be URL encoded. -db_req(#httpd{method='GET',mochi_req=MochiReq, path_parts=[DbName,<<"_design/",_/binary>>|_]}=Req, _Db) -> - PathFront = "/" ++ couch_httpd:quote(binary_to_list(DbName)) ++ "/", - [_|PathTail] = re:split(MochiReq:get(raw_path), "_design%2F", - [{return, list}]), - couch_httpd:send_redirect(Req, PathFront ++ "_design/" ++ - mochiweb_util:join(PathTail, "_design%2F")); - -db_req(#httpd{path_parts=[_DbName,<<"_design">>,Name]}=Req, Db) -> - db_doc_req(Req, Db, <<"_design/",Name/binary>>); - -db_req(#httpd{path_parts=[_DbName,<<"_design">>,Name|FileNameParts]}=Req, Db) -> - db_attachment_req(Req, Db, <<"_design/",Name/binary>>, FileNameParts); - - -% Special case to allow for accessing local documents without %2F -% encoding the docid. Throws out requests that don't have the second -% path part or that specify an attachment name. -db_req(#httpd{path_parts=[_DbName, <<"_local">>]}, _Db) -> - throw({bad_request, <<"Invalid _local document id.">>}); - -db_req(#httpd{path_parts=[_DbName, <<"_local/">>]}, _Db) -> - throw({bad_request, <<"Invalid _local document id.">>}); - -db_req(#httpd{path_parts=[_DbName, <<"_local">>, Name]}=Req, Db) -> - db_doc_req(Req, Db, <<"_local/", Name/binary>>); - -db_req(#httpd{path_parts=[_DbName, <<"_local">> | _Rest]}, _Db) -> - throw({bad_request, <<"_local documents do not accept attachments.">>}); - -db_req(#httpd{path_parts=[_, DocId]}=Req, Db) -> - db_doc_req(Req, Db, DocId); - -db_req(#httpd{path_parts=[_, DocId | FileNameParts]}=Req, Db) -> - db_attachment_req(Req, Db, DocId, FileNameParts). - -db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) -> - % check for the existence of the doc to handle the 404 case. - couch_doc_open(Db, DocId, nil, []), - case couch_httpd:qs_value(Req, "rev") of - undefined -> - update_doc(Req, Db, DocId, - couch_doc_from_req(Req, DocId, {[{<<"_deleted">>,true}]})); - Rev -> - update_doc(Req, Db, DocId, - couch_doc_from_req(Req, DocId, - {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]})) - end; - -db_doc_req(#httpd{method = 'GET', mochi_req = MochiReq} = Req, Db, DocId) -> - #doc_query_args{ - rev = Rev, - open_revs = Revs, - options = Options1, - atts_since = AttsSince - } = parse_doc_query(Req), - Options = case AttsSince of - nil -> - Options1; - RevList when is_list(RevList) -> - [{atts_since, RevList}, attachments | Options1] - end, - case Revs of - [] -> - Doc = couch_doc_open(Db, DocId, Rev, Options), - send_doc(Req, Doc, Options); - _ -> - {ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options), - case MochiReq:accepts_content_type("multipart/mixed") of - false -> - {ok, Resp} = start_json_response(Req, 200), - send_chunk(Resp, "["), - % We loop through the docs. The first time through the separator - % is whitespace, then a comma on subsequent iterations. - lists:foldl( - fun(Result, AccSeparator) -> - case Result of - {ok, Doc} -> - JsonDoc = couch_doc:to_json_obj(Doc, Options), - Json = ?JSON_ENCODE({[{ok, JsonDoc}]}), - send_chunk(Resp, AccSeparator ++ Json); - {{not_found, missing}, RevId} -> - RevStr = couch_doc:rev_to_str(RevId), - Json = ?JSON_ENCODE({[{"missing", RevStr}]}), - send_chunk(Resp, AccSeparator ++ Json) - end, - "," % AccSeparator now has a comma - end, - "", Results), - send_chunk(Resp, "]"), - end_json_response(Resp); - true -> - send_docs_multipart(Req, Results, Options) - end - end; - - -db_doc_req(#httpd{method='POST'}=Req, Db, DocId) -> - couch_httpd:validate_referer(Req), - couch_doc:validate_docid(DocId), - couch_httpd:validate_ctype(Req, "multipart/form-data"), - Form = couch_httpd:parse_form(Req), - case couch_util:get_value("_doc", Form) of - undefined -> - Rev = couch_doc:parse_rev(couch_util:get_value("_rev", Form)), - {ok, [{ok, Doc}]} = couch_db:open_doc_revs(Db, DocId, [Rev], []); - Json -> - Doc = couch_doc_from_req(Req, DocId, ?JSON_DECODE(Json)) - end, - UpdatedAtts = [ - #att{name=validate_attachment_name(Name), - type=list_to_binary(ContentType), - data=Content} || - {Name, {ContentType, _}, Content} <- - proplists:get_all_values("_attachments", Form) - ], - #doc{atts=OldAtts} = Doc, - OldAtts2 = lists:flatmap( - fun(#att{name=OldName}=Att) -> - case [1 || A <- UpdatedAtts, A#att.name == OldName] of - [] -> [Att]; % the attachment wasn't in the UpdatedAtts, return it - _ -> [] % the attachment was in the UpdatedAtts, drop it - end - end, OldAtts), - NewDoc = Doc#doc{ - atts = UpdatedAtts ++ OldAtts2 - }, - update_doc(Req, Db, DocId, NewDoc); - -db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) -> - couch_doc:validate_docid(DocId), - - case couch_util:to_list(couch_httpd:header_value(Req, "Content-Type")) of - ("multipart/related;" ++ _) = ContentType -> - {ok, Doc0, WaitFun, Parser} = couch_doc:doc_from_multi_part_stream( - ContentType, fun() -> receive_request_data(Req) end), - Doc = couch_doc_from_req(Req, DocId, Doc0), - try - Result = update_doc(Req, Db, DocId, Doc), - WaitFun(), - Result - catch throw:Err -> - % Document rejected by a validate_doc_update function. - couch_doc:abort_multi_part_stream(Parser), - throw(Err) - end; - _Else -> - Body = couch_httpd:json_body(Req), - Doc = couch_doc_from_req(Req, DocId, Body), - update_doc(Req, Db, DocId, Doc) - end; - -db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) -> - SourceRev = - case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of - missing_rev -> nil; - Rev -> Rev - end, - {TargetDocId, TargetRevs} = parse_copy_destination_header(Req), - % open old doc - Doc = couch_doc_open(Db, SourceDocId, SourceRev, []), - % save new doc - update_doc(Req, Db, TargetDocId, Doc#doc{id=TargetDocId, revs=TargetRevs}); - -db_doc_req(Req, _Db, _DocId) -> - send_method_not_allowed(Req, "DELETE,GET,HEAD,POST,PUT,COPY"). - - -send_doc(Req, Doc, Options) -> - case Doc#doc.meta of - [] -> - DiskEtag = couch_httpd:doc_etag(Doc), - % output etag only when we have no meta - couch_httpd:etag_respond(Req, DiskEtag, fun() -> - send_doc_efficiently(Req, Doc, [{"ETag", DiskEtag}], Options) - end); - _ -> - send_doc_efficiently(Req, Doc, [], Options) - end. - - -send_doc_efficiently(Req, #doc{atts=[]}=Doc, Headers, Options) -> - send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options)); -send_doc_efficiently(#httpd{mochi_req = MochiReq} = Req, - #doc{atts = Atts} = Doc, Headers, Options) -> - case lists:member(attachments, Options) of - true -> - case MochiReq:accepts_content_type("multipart/related") of - false -> - send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options)); - true -> - Boundary = couch_uuids:random(), - JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, - [attachments, follows, att_encoding_info | Options])), - {ContentType, Len} = couch_doc:len_doc_to_multi_part_stream( - Boundary,JsonBytes, Atts, true), - CType = {"Content-Type", ?b2l(ContentType)}, - {ok, Resp} = start_response_length(Req, 200, [CType|Headers], Len), - couch_doc:doc_to_multi_part_stream(Boundary,JsonBytes,Atts, - fun(Data) -> couch_httpd:send(Resp, Data) end, true) - end; - false -> - send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options)) - end. - -send_docs_multipart(Req, Results, Options1) -> - OuterBoundary = couch_uuids:random(), - InnerBoundary = couch_uuids:random(), - Options = [attachments, follows, att_encoding_info | Options1], - CType = {"Content-Type", - "multipart/mixed; boundary=\"" ++ ?b2l(OuterBoundary) ++ "\""}, - {ok, Resp} = start_chunked_response(Req, 200, [CType]), - couch_httpd:send_chunk(Resp, <<"--", OuterBoundary/binary>>), - lists:foreach( - fun({ok, #doc{atts=Atts}=Doc}) -> - JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, Options)), - {ContentType, _Len} = couch_doc:len_doc_to_multi_part_stream( - InnerBoundary, JsonBytes, Atts, true), - couch_httpd:send_chunk(Resp, <<"\r\nContent-Type: ", - ContentType/binary, "\r\n\r\n">>), - couch_doc:doc_to_multi_part_stream(InnerBoundary, JsonBytes, Atts, - fun(Data) -> couch_httpd:send_chunk(Resp, Data) - end, true), - couch_httpd:send_chunk(Resp, <<"\r\n--", OuterBoundary/binary>>); - ({{not_found, missing}, RevId}) -> - RevStr = couch_doc:rev_to_str(RevId), - Json = ?JSON_ENCODE({[{"missing", RevStr}]}), - couch_httpd:send_chunk(Resp, - [<<"\r\nContent-Type: application/json; error=\"true\"\r\n\r\n">>, - Json, - <<"\r\n--", OuterBoundary/binary>>]) - end, Results), - couch_httpd:send_chunk(Resp, <<"--">>), - couch_httpd:last_chunk(Resp). - -send_ranges_multipart(Req, ContentType, Len, Att, Ranges) -> - Boundary = couch_uuids:random(), - CType = {"Content-Type", - "multipart/byteranges; boundary=\"" ++ ?b2l(Boundary) ++ "\""}, - {ok, Resp} = start_chunked_response(Req, 206, [CType]), - couch_httpd:send_chunk(Resp, <<"--", Boundary/binary>>), - lists:foreach(fun({From, To}) -> - ContentRange = ?l2b(make_content_range(From, To, Len)), - couch_httpd:send_chunk(Resp, - <<"\r\nContent-Type: ", ContentType/binary, "\r\n", - "Content-Range: ", ContentRange/binary, "\r\n", - "\r\n">>), - couch_doc:range_att_foldl(Att, From, To + 1, - fun(Seg, _) -> send_chunk(Resp, Seg) end, {ok, Resp}), - couch_httpd:send_chunk(Resp, <<"\r\n--", Boundary/binary>>) - end, Ranges), - couch_httpd:send_chunk(Resp, <<"--">>), - couch_httpd:last_chunk(Resp), - {ok, Resp}. - -receive_request_data(Req) -> - receive_request_data(Req, couch_httpd:body_length(Req)). - -receive_request_data(Req, LenLeft) when LenLeft > 0 -> - Len = erlang:min(4096, LenLeft), - Data = couch_httpd:recv(Req, Len), - {Data, fun() -> receive_request_data(Req, LenLeft - iolist_size(Data)) end}; -receive_request_data(_Req, _) -> - throw(<<"expected more data">>). - -make_content_range(From, To, Len) -> - io_lib:format("bytes ~B-~B/~B", [From, To, Len]). - -update_doc_result_to_json({{Id, Rev}, Error}) -> - {_Code, Err, Msg} = couch_httpd:error_info(Error), - {[{id, Id}, {rev, couch_doc:rev_to_str(Rev)}, - {error, Err}, {reason, Msg}]}. - -update_doc_result_to_json(#doc{id=DocId}, Result) -> - update_doc_result_to_json(DocId, Result); -update_doc_result_to_json(DocId, {ok, NewRev}) -> - {[{ok, true}, {id, DocId}, {rev, couch_doc:rev_to_str(NewRev)}]}; -update_doc_result_to_json(DocId, Error) -> - {_Code, ErrorStr, Reason} = couch_httpd:error_info(Error), - {[{id, DocId}, {error, ErrorStr}, {reason, Reason}]}. - - -update_doc(Req, Db, DocId, #doc{deleted=false}=Doc) -> - Loc = absolute_uri(Req, "/" ++ ?b2l(Db#db.name) ++ "/" ++ ?b2l(DocId)), - update_doc(Req, Db, DocId, Doc, [{"Location", Loc}]); -update_doc(Req, Db, DocId, Doc) -> - update_doc(Req, Db, DocId, Doc, []). - -update_doc(Req, Db, DocId, Doc, Headers) -> - #doc_query_args{ - update_type = UpdateType - } = parse_doc_query(Req), - update_doc(Req, Db, DocId, Doc, Headers, UpdateType). - -update_doc(Req, Db, DocId, #doc{deleted=Deleted}=Doc, Headers, UpdateType) -> - case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of - "true" -> - Options = [full_commit]; - "false" -> - Options = [delay_commit]; - _ -> - Options = [] - end, - case couch_httpd:qs_value(Req, "batch") of - "ok" -> - % async batching - spawn(fun() -> - case catch(couch_db:update_doc(Db, Doc, Options, UpdateType)) of - {ok, _} -> ok; - Error -> - ?LOG_INFO("Batch doc error (~s): ~p",[DocId, Error]) - end - end), - send_json(Req, 202, Headers, {[ - {ok, true}, - {id, DocId} - ]}); - _Normal -> - % normal - {ok, NewRev} = couch_db:update_doc(Db, Doc, Options, UpdateType), - NewRevStr = couch_doc:rev_to_str(NewRev), - ResponseHeaders = [{"ETag", <<"\"", NewRevStr/binary, "\"">>}] ++ Headers, - send_json(Req, - if Deleted orelse Req#httpd.method == 'DELETE' -> 200; - true -> 201 end, - ResponseHeaders, {[ - {ok, true}, - {id, DocId}, - {rev, NewRevStr}]}) - end. - -couch_doc_from_req(Req, DocId, #doc{revs=Revs}=Doc) -> - validate_attachment_names(Doc), - Rev = case couch_httpd:qs_value(Req, "rev") of - undefined -> - undefined; - QSRev -> - couch_doc:parse_rev(QSRev) - end, - Revs2 = - case Revs of - {Start, [RevId|_]} -> - if Rev /= undefined andalso Rev /= {Start, RevId} -> - throw({bad_request, "Document rev from request body and query " - "string have different values"}); - true -> - case extract_header_rev(Req, {Start, RevId}) of - missing_rev -> {0, []}; - _ -> Revs - end - end; - _ -> - case extract_header_rev(Req, Rev) of - missing_rev -> {0, []}; - {Pos, RevId2} -> {Pos, [RevId2]} - end - end, - Doc#doc{id=DocId, revs=Revs2}; -couch_doc_from_req(Req, DocId, Json) -> - couch_doc_from_req(Req, DocId, couch_doc:from_json_obj(Json)). - -% Useful for debugging -% couch_doc_open(Db, DocId) -> -% couch_doc_open(Db, DocId, nil, []). - -couch_doc_open(Db, DocId, Rev, Options) -> - case Rev of - nil -> % open most recent rev - case couch_db:open_doc(Db, DocId, Options) of - {ok, Doc} -> - Doc; - Error -> - throw(Error) - end; - _ -> % open a specific rev (deletions come back as stubs) - case couch_db:open_doc_revs(Db, DocId, [Rev], Options) of - {ok, [{ok, Doc}]} -> - Doc; - {ok, [{{not_found, missing}, Rev}]} -> - throw(not_found); - {ok, [Else]} -> - throw(Else) - end - end. - -% Attachment request handlers - -db_attachment_req(#httpd{method='GET',mochi_req=MochiReq}=Req, Db, DocId, FileNameParts) -> - FileName = list_to_binary(mochiweb_util:join(lists:map(fun binary_to_list/1, FileNameParts),"/")), - #doc_query_args{ - rev=Rev, - options=Options - } = parse_doc_query(Req), - #doc{ - atts=Atts - } = Doc = couch_doc_open(Db, DocId, Rev, Options), - case [A || A <- Atts, A#att.name == FileName] of - [] -> - throw({not_found, "Document is missing attachment"}); - [#att{type=Type, encoding=Enc, disk_len=DiskLen, att_len=AttLen}=Att] -> - Etag = case Att#att.md5 of - <<>> -> couch_httpd:doc_etag(Doc); - Md5 -> "\"" ++ ?b2l(base64:encode(Md5)) ++ "\"" - end, - ReqAcceptsAttEnc = lists:member( - atom_to_list(Enc), - couch_httpd:accepted_encodings(Req) - ), - Len = case {Enc, ReqAcceptsAttEnc} of - {identity, _} -> - % stored and served in identity form - DiskLen; - {_, false} when DiskLen =/= AttLen -> - % Stored encoded, but client doesn't accept the encoding we used, - % so we need to decode on the fly. DiskLen is the identity length - % of the attachment. - DiskLen; - {_, true} -> - % Stored and served encoded. AttLen is the encoded length. - AttLen; - _ -> - % We received an encoded attachment and stored it as such, so we - % don't know the identity length. The client doesn't accept the - % encoding, and since we cannot serve a correct Content-Length - % header we'll fall back to a chunked response. - undefined - end, - Headers = [ - {"ETag", Etag}, - {"Cache-Control", "must-revalidate"}, - {"Content-Type", binary_to_list(Type)} - ] ++ case ReqAcceptsAttEnc of - true when Enc =/= identity -> - % RFC 2616 says that the 'identify' encoding should not be used in - % the Content-Encoding header - [{"Content-Encoding", atom_to_list(Enc)}]; - _ -> - [] - end ++ case Enc of - identity -> - [{"Accept-Ranges", "bytes"}]; - _ -> - [{"Accept-Ranges", "none"}] - end, - AttFun = case ReqAcceptsAttEnc of - false -> - fun couch_doc:att_foldl_decode/3; - true -> - fun couch_doc:att_foldl/3 - end, - couch_httpd:etag_respond( - Req, - Etag, - fun() -> - case Len of - undefined -> - {ok, Resp} = start_chunked_response(Req, 200, Headers), - AttFun(Att, fun(Seg, _) -> send_chunk(Resp, Seg) end, {ok, Resp}), - last_chunk(Resp); - _ -> - Ranges = parse_ranges(MochiReq:get(range), Len), - case {Enc, Ranges} of - {identity, [{From, To}]} -> - Headers1 = [{"Content-Range", make_content_range(From, To, Len)}] - ++ Headers, - {ok, Resp} = start_response_length(Req, 206, Headers1, To - From + 1), - couch_doc:range_att_foldl(Att, From, To + 1, - fun(Seg, _) -> send(Resp, Seg) end, {ok, Resp}); - {identity, Ranges} when is_list(Ranges) andalso length(Ranges) < 10 -> - send_ranges_multipart(Req, Type, Len, Att, Ranges); - _ -> - Headers1 = Headers ++ - if Enc =:= identity orelse ReqAcceptsAttEnc =:= true -> - [{"Content-MD5", base64:encode(Att#att.md5)}]; - true -> - [] - end, - {ok, Resp} = start_response_length(Req, 200, Headers1, Len), - AttFun(Att, fun(Seg, _) -> send(Resp, Seg) end, {ok, Resp}) - end - end - end - ) - end; - - -db_attachment_req(#httpd{method=Method,mochi_req=MochiReq}=Req, Db, DocId, FileNameParts) - when (Method == 'PUT') or (Method == 'DELETE') -> - FileName = validate_attachment_name( - mochiweb_util:join( - lists:map(fun binary_to_list/1, - FileNameParts),"/")), - - NewAtt = case Method of - 'DELETE' -> - []; - _ -> - [#att{ - name = FileName, - type = case couch_httpd:header_value(Req,"Content-Type") of - undefined -> - % We could throw an error here or guess by the FileName. - % Currently, just giving it a default. - <<"application/octet-stream">>; - CType -> - list_to_binary(CType) - end, - data = case couch_httpd:body_length(Req) of - undefined -> - <<"">>; - {unknown_transfer_encoding, Unknown} -> - exit({unknown_transfer_encoding, Unknown}); - chunked -> - fun(MaxChunkSize, ChunkFun, InitState) -> - couch_httpd:recv_chunked(Req, MaxChunkSize, - ChunkFun, InitState) - end; - 0 -> - <<"">>; - Length when is_integer(Length) -> - Expect = case couch_httpd:header_value(Req, "expect") of - undefined -> - undefined; - Value when is_list(Value) -> - string:to_lower(Value) - end, - case Expect of - "100-continue" -> - MochiReq:start_raw_response({100, gb_trees:empty()}); - _Else -> - ok - end, - - - fun(Size) -> couch_httpd:recv(Req, Size) end - end, - att_len = case couch_httpd:header_value(Req,"Content-Length") of - undefined -> - undefined; - Length -> - list_to_integer(Length) - end, - md5 = get_md5_header(Req), - encoding = case string:to_lower(string:strip( - couch_httpd:header_value(Req,"Content-Encoding","identity") - )) of - "identity" -> - identity; - "gzip" -> - gzip; - _ -> - throw({ - bad_ctype, - "Only gzip and identity content-encodings are supported" - }) - end - }] - end, - - Doc = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of - missing_rev -> % make the new doc - couch_doc:validate_docid(DocId), - #doc{id=DocId}; - Rev -> - case couch_db:open_doc_revs(Db, DocId, [Rev], []) of - {ok, [{ok, Doc0}]} -> Doc0; - {ok, [{{not_found, missing}, Rev}]} -> throw(conflict); - {ok, [Error]} -> throw(Error) - end - end, - - #doc{atts=Atts} = Doc, - DocEdited = Doc#doc{ - atts = NewAtt ++ [A || A <- Atts, A#att.name /= FileName] - }, - - Headers = case Method of - 'DELETE' -> - []; - _ -> - [{"Location", absolute_uri(Req, "/" ++ - ?b2l(Db#db.name) ++ "/" ++ - ?b2l(DocId) ++ "/" ++ - ?b2l(FileName) - )}] - end, - update_doc(Req, Db, DocId, DocEdited, Headers); - -db_attachment_req(Req, _Db, _DocId, _FileNameParts) -> - send_method_not_allowed(Req, "DELETE,GET,HEAD,PUT"). - -parse_ranges(undefined, _Len) -> - undefined; -parse_ranges(fail, _Len) -> - undefined; -parse_ranges(Ranges, Len) -> - parse_ranges(Ranges, Len, []). - -parse_ranges([], _Len, Acc) -> - lists:reverse(Acc); -parse_ranges([{0, none}|_], _Len, _Acc) -> - undefined; -parse_ranges([{From, To}|_], _Len, _Acc) when is_integer(From) andalso is_integer(To) andalso To < From -> - throw(requested_range_not_satisfiable); -parse_ranges([{From, To}|Rest], Len, Acc) when is_integer(To) andalso To >= Len -> - parse_ranges([{From, Len-1}] ++ Rest, Len, Acc); -parse_ranges([{none, To}|Rest], Len, Acc) -> - parse_ranges([{Len - To, Len - 1}] ++ Rest, Len, Acc); -parse_ranges([{From, none}|Rest], Len, Acc) -> - parse_ranges([{From, Len - 1}] ++ Rest, Len, Acc); -parse_ranges([{From,To}|Rest], Len, Acc) -> - parse_ranges(Rest, Len, [{From, To}] ++ Acc). - -get_md5_header(Req) -> - ContentMD5 = couch_httpd:header_value(Req, "Content-MD5"), - Length = couch_httpd:body_length(Req), - Trailer = couch_httpd:header_value(Req, "Trailer"), - case {ContentMD5, Length, Trailer} of - _ when is_list(ContentMD5) orelse is_binary(ContentMD5) -> - base64:decode(ContentMD5); - {_, chunked, undefined} -> - <<>>; - {_, chunked, _} -> - case re:run(Trailer, "\\bContent-MD5\\b", [caseless]) of - {match, _} -> - md5_in_footer; - _ -> - <<>> - end; - _ -> - <<>> - end. - -parse_doc_query(Req) -> - lists:foldl(fun({Key,Value}, Args) -> - case {Key, Value} of - {"attachments", "true"} -> - Options = [attachments | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - {"meta", "true"} -> - Options = [revs_info, conflicts, deleted_conflicts | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - {"revs", "true"} -> - Options = [revs | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - {"local_seq", "true"} -> - Options = [local_seq | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - {"revs_info", "true"} -> - Options = [revs_info | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - {"conflicts", "true"} -> - Options = [conflicts | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - {"deleted_conflicts", "true"} -> - Options = [deleted_conflicts | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - {"rev", Rev} -> - Args#doc_query_args{rev=couch_doc:parse_rev(Rev)}; - {"open_revs", "all"} -> - Args#doc_query_args{open_revs=all}; - {"open_revs", RevsJsonStr} -> - JsonArray = ?JSON_DECODE(RevsJsonStr), - Args#doc_query_args{open_revs=couch_doc:parse_revs(JsonArray)}; - {"latest", "true"} -> - Options = [latest | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - {"atts_since", RevsJsonStr} -> - JsonArray = ?JSON_DECODE(RevsJsonStr), - Args#doc_query_args{atts_since = couch_doc:parse_revs(JsonArray)}; - {"new_edits", "false"} -> - Args#doc_query_args{update_type=replicated_changes}; - {"new_edits", "true"} -> - Args#doc_query_args{update_type=interactive_edit}; - {"att_encoding_info", "true"} -> - Options = [att_encoding_info | Args#doc_query_args.options], - Args#doc_query_args{options=Options}; - _Else -> % unknown key value pair, ignore. - Args - end - end, #doc_query_args{}, couch_httpd:qs(Req)). - -parse_changes_query(Req, Db) -> - ChangesArgs = lists:foldl(fun({Key, Value}, Args) -> - case {string:to_lower(Key), Value} of - {"feed", _} -> - Args#changes_args{feed=Value}; - {"descending", "true"} -> - Args#changes_args{dir=rev}; - {"since", "now"} -> - UpdateSeq = couch_util:with_db(Db#db.name, fun(WDb) -> - couch_db:get_update_seq(WDb) - end), - Args#changes_args{since=UpdateSeq}; - {"since", _} -> - Args#changes_args{since=list_to_integer(Value)}; - {"last-event-id", _} -> - Args#changes_args{since=list_to_integer(Value)}; - {"limit", _} -> - Args#changes_args{limit=list_to_integer(Value)}; - {"style", _} -> - Args#changes_args{style=list_to_existing_atom(Value)}; - {"heartbeat", "true"} -> - Args#changes_args{heartbeat=true}; - {"heartbeat", _} -> - Args#changes_args{heartbeat=list_to_integer(Value)}; - {"timeout", _} -> - Args#changes_args{timeout=list_to_integer(Value)}; - {"include_docs", "true"} -> - Args#changes_args{include_docs=true}; - {"attachments", "true"} -> - Opts = Args#changes_args.doc_options, - Args#changes_args{doc_options=[attachments|Opts]}; - {"att_encoding_info", "true"} -> - Opts = Args#changes_args.doc_options, - Args#changes_args{doc_options=[att_encoding_info|Opts]}; - {"conflicts", "true"} -> - Args#changes_args{conflicts=true}; - {"filter", _} -> - Args#changes_args{filter=Value}; - _Else -> % unknown key value pair, ignore. - Args - end - end, #changes_args{}, couch_httpd:qs(Req)), - %% if it's an EventSource request with a Last-event-ID header - %% that should override the `since` query string, since it's - %% probably the browser reconnecting. - case ChangesArgs#changes_args.feed of - "eventsource" -> - case couch_httpd:header_value(Req, "last-event-id") of - undefined -> - ChangesArgs; - Value -> - ChangesArgs#changes_args{since=list_to_integer(Value)} - end; - _ -> - ChangesArgs - end. - -extract_header_rev(Req, ExplicitRev) when is_binary(ExplicitRev) or is_list(ExplicitRev)-> - extract_header_rev(Req, couch_doc:parse_rev(ExplicitRev)); -extract_header_rev(Req, ExplicitRev) -> - Etag = case couch_httpd:header_value(Req, "If-Match") of - undefined -> undefined; - Value -> couch_doc:parse_rev(string:strip(Value, both, $")) - end, - case {ExplicitRev, Etag} of - {undefined, undefined} -> missing_rev; - {_, undefined} -> ExplicitRev; - {undefined, _} -> Etag; - _ when ExplicitRev == Etag -> Etag; - _ -> - throw({bad_request, "Document rev and etag have different values"}) - end. - - -parse_copy_destination_header(Req) -> - case couch_httpd:header_value(Req, "Destination") of - undefined -> - throw({bad_request, "Destination header is mandatory for COPY."}); - Destination -> - case re:run(Destination, "^https?://", [{capture, none}]) of - match -> - throw({bad_request, "Destination URL must be relative."}); - nomatch -> - % see if ?rev=revid got appended to the Destination header - case re:run(Destination, "\\?", [{capture, none}]) of - nomatch -> - {list_to_binary(Destination), {0, []}}; - match -> - [DocId, RevQs] = re:split(Destination, "\\?", [{return, list}]), - [_RevQueryKey, Rev] = re:split(RevQs, "=", [{return, list}]), - {Pos, RevId} = couch_doc:parse_rev(Rev), - {list_to_binary(DocId), {Pos, [RevId]}} - end - end - end. - -validate_attachment_names(Doc) -> - lists:foreach(fun(#att{name=Name}) -> - validate_attachment_name(Name) - end, Doc#doc.atts). - -validate_attachment_name(Name) when is_list(Name) -> - validate_attachment_name(list_to_binary(Name)); -validate_attachment_name(<<"_",_/binary>>) -> - throw({bad_request, <<"Attachment name can't start with '_'">>}); -validate_attachment_name(Name) -> - case couch_util:validate_utf8(Name) of - true -> Name; - false -> throw({bad_request, <<"Attachment name is not UTF-8 encoded">>}) - end. - http://git-wip-us.apache.org/repos/asf/couchdb/blob/5618e610/src/apps/couch/src/couch_httpd_external.erl ---------------------------------------------------------------------- diff --git a/src/apps/couch/src/couch_httpd_external.erl b/src/apps/couch/src/couch_httpd_external.erl deleted file mode 100644 index 2036d25..0000000 --- a/src/apps/couch/src/couch_httpd_external.erl +++ /dev/null @@ -1,177 +0,0 @@ -% Licensed under the Apache License, Version 2.0 (the "License"); you may not -% use this file except in compliance with the License. You may obtain a copy of -% the License at -% -% http://www.apache.org/licenses/LICENSE-2.0 -% -% Unless required by applicable law or agreed to in writing, software -% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -% License for the specific language governing permissions and limitations under -% the License. - --module(couch_httpd_external). - --export([handle_external_req/2, handle_external_req/3]). --export([send_external_response/2, json_req_obj/2, json_req_obj/3]). --export([default_or_content_type/2, parse_external_response/1]). - --import(couch_httpd,[send_error/4]). - --include("couch_db.hrl"). - -% handle_external_req/2 -% for the old type of config usage: -% _external = {couch_httpd_external, handle_external_req} -% with urls like -% /db/_external/action/design/name -handle_external_req(#httpd{ - path_parts=[_DbName, _External, UrlName | _Path] - }=HttpReq, Db) -> - process_external_req(HttpReq, Db, UrlName); -handle_external_req(#httpd{path_parts=[_, _]}=Req, _Db) -> - send_error(Req, 404, <<"external_server_error">>, <<"No server name specified.">>); -handle_external_req(Req, _) -> - send_error(Req, 404, <<"external_server_error">>, <<"Broken assumption">>). - -% handle_external_req/3 -% for this type of config usage: -% _action = {couch_httpd_external, handle_external_req, <<"action">>} -% with urls like -% /db/_action/design/name -handle_external_req(HttpReq, Db, Name) -> - process_external_req(HttpReq, Db, Name). - -process_external_req(HttpReq, Db, Name) -> - - Response = couch_external_manager:execute(binary_to_list(Name), - json_req_obj(HttpReq, Db)), - - case Response of - {unknown_external_server, Msg} -> - send_error(HttpReq, 404, <<"external_server_error">>, Msg); - _ -> - send_external_response(HttpReq, Response) - end. -json_req_obj(Req, Db) -> json_req_obj(Req, Db, null). -json_req_obj(#httpd{mochi_req=Req, - method=Method, - requested_path_parts=RequestedPath, - path_parts=Path, - req_body=ReqBody - }, Db, DocId) -> - Body = case ReqBody of - undefined -> - MaxSize = list_to_integer( - couch_config:get("couchdb", "max_document_size", "4294967296")), - Req:recv_body(MaxSize); - Else -> Else - end, - ParsedForm = case Req:get_primary_header_value("content-type") of - "application/x-www-form-urlencoded" ++ _ -> - case Body of - undefined -> []; - _ -> mochiweb_util:parse_qs(Body) - end; - _ -> - [] - end, - Headers = Req:get(headers), - Hlist = mochiweb_headers:to_list(Headers), - {ok, Info} = couch_db:get_db_info(Db), - -% add headers... - {[{<<"info">>, {Info}}, - {<<"id">>, DocId}, - {<<"uuid">>, couch_uuids:new()}, - {<<"method">>, Method}, - {<<"requested_path">>, RequestedPath}, - {<<"path">>, Path}, - {<<"raw_path">>, ?l2b(Req:get(raw_path))}, - {<<"query">>, json_query_keys(to_json_terms(Req:parse_qs()))}, - {<<"headers">>, to_json_terms(Hlist)}, - {<<"body">>, Body}, - {<<"peer">>, ?l2b(Req:get(peer))}, - {<<"form">>, to_json_terms(ParsedForm)}, - {<<"cookie">>, to_json_terms(Req:parse_cookie())}, - {<<"userCtx">>, couch_util:json_user_ctx(Db)}, - {<<"secObj">>, couch_db:get_security(Db)}]}. - -to_json_terms(Data) -> - to_json_terms(Data, []). - -to_json_terms([], Acc) -> - {lists:reverse(Acc)}; -to_json_terms([{Key, Value} | Rest], Acc) when is_atom(Key) -> - to_json_terms(Rest, [{list_to_binary(atom_to_list(Key)), list_to_binary(Value)} | Acc]); -to_json_terms([{Key, Value} | Rest], Acc) -> - to_json_terms(Rest, [{list_to_binary(Key), list_to_binary(Value)} | Acc]). - -json_query_keys({Json}) -> - json_query_keys(Json, []). -json_query_keys([], Acc) -> - {lists:reverse(Acc)}; -json_query_keys([{<<"startkey">>, Value} | Rest], Acc) -> - json_query_keys(Rest, [{<<"startkey">>, ?JSON_DECODE(Value)}|Acc]); -json_query_keys([{<<"endkey">>, Value} | Rest], Acc) -> - json_query_keys(Rest, [{<<"endkey">>, ?JSON_DECODE(Value)}|Acc]); -json_query_keys([{<<"key">>, Value} | Rest], Acc) -> - json_query_keys(Rest, [{<<"key">>, ?JSON_DECODE(Value)}|Acc]); -json_query_keys([Term | Rest], Acc) -> - json_query_keys(Rest, [Term|Acc]). - -send_external_response(Req, Response) -> - #extern_resp_args{ - code = Code, - data = Data, - ctype = CType, - headers = Headers, - json = Json - } = parse_external_response(Response), - Headers1 = default_or_content_type(CType, Headers), - case Json of - nil -> - couch_httpd:send_response(Req, Code, Headers1, Data); - Json -> - couch_httpd:send_json(Req, Code, Headers1, Json) - end. - -parse_external_response({Response}) -> - lists:foldl(fun({Key,Value}, Args) -> - case {Key, Value} of - {"", _} -> - Args; - {<<"code">>, Value} -> - Args#extern_resp_args{code=Value}; - {<<"stop">>, true} -> - Args#extern_resp_args{stop=true}; - {<<"json">>, Value} -> - Args#extern_resp_args{ - json=Value, - ctype="application/json"}; - {<<"body">>, Value} -> - Args#extern_resp_args{data=Value, ctype="text/html; charset=utf-8"}; - {<<"base64">>, Value} -> - Args#extern_resp_args{ - data=base64:decode(Value), - ctype="application/binary" - }; - {<<"headers">>, {Headers}} -> - NewHeaders = lists:map(fun({Header, HVal}) -> - {binary_to_list(Header), binary_to_list(HVal)} - end, Headers), - Args#extern_resp_args{headers=NewHeaders}; - _ -> % unknown key - Msg = lists:flatten(io_lib:format("Invalid data from external server: ~p", [{Key, Value}])), - throw({external_response_error, Msg}) - end - end, #extern_resp_args{}, Response). - -default_or_content_type(DefaultContentType, Headers) -> - IsContentType = fun({X, _}) -> string:to_lower(X) == "content-type" end, - case lists:any(IsContentType, Headers) of - false -> - [{"Content-Type", DefaultContentType} | Headers]; - true -> - Headers - end. http://git-wip-us.apache.org/repos/asf/couchdb/blob/5618e610/src/apps/couch/src/couch_httpd_misc_handlers.erl ---------------------------------------------------------------------- diff --git a/src/apps/couch/src/couch_httpd_misc_handlers.erl b/src/apps/couch/src/couch_httpd_misc_handlers.erl deleted file mode 100644 index 96a05c6..0000000 --- a/src/apps/couch/src/couch_httpd_misc_handlers.erl +++ /dev/null @@ -1,318 +0,0 @@ -% Licensed under the Apache License, Version 2.0 (the "License"); you may not -% use this file except in compliance with the License. You may obtain a copy of -% the License at -% -% http://www.apache.org/licenses/LICENSE-2.0 -% -% Unless required by applicable law or agreed to in writing, software -% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -% License for the specific language governing permissions and limitations under -% the License. - --module(couch_httpd_misc_handlers). - --export([handle_welcome_req/2,handle_favicon_req/2,handle_utils_dir_req/2, - handle_all_dbs_req/1,handle_restart_req/1, - handle_uuids_req/1,handle_config_req/1,handle_log_req/1, - handle_task_status_req/1, handle_file_req/2]). - --export([increment_update_seq_req/2]). - - --include("couch_db.hrl"). - --import(couch_httpd, - [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, - start_json_response/2,send_chunk/2,last_chunk/1,end_json_response/1, - start_chunked_response/3, send_error/4]). - -% httpd global handlers - -handle_welcome_req(#httpd{method='GET'}=Req, WelcomeMessage) -> - send_json(Req, {[ - {couchdb, WelcomeMessage}, - {uuid, couch_server:get_uuid()}, - {version, list_to_binary(couch_server:get_version())} - ] ++ case couch_config:get("vendor") of - [] -> - []; - Properties -> - [{vendor, {[{?l2b(K), ?l2b(V)} || {K, V} <- Properties]}}] - end - }); -handle_welcome_req(Req, _) -> - send_method_not_allowed(Req, "GET,HEAD"). - -handle_favicon_req(#httpd{method='GET'}=Req, DocumentRoot) -> - {{Year,Month,Day},Time} = erlang:universaltime(), - OneYearFromNow = {{Year+1,Month,Day},Time}, - CachingHeaders = [ - %favicon should expire a year from now - {"Cache-Control", "public, max-age=31536000"}, - {"Expires", couch_util:rfc1123_date(OneYearFromNow)} - ], - couch_httpd:serve_file(Req, "favicon.ico", DocumentRoot, CachingHeaders); - -handle_favicon_req(Req, _) -> - send_method_not_allowed(Req, "GET,HEAD"). - -handle_file_req(#httpd{method='GET'}=Req, Document) -> - couch_httpd:serve_file(Req, filename:basename(Document), filename:dirname(Document)); - -handle_file_req(Req, _) -> - send_method_not_allowed(Req, "GET,HEAD"). - -handle_utils_dir_req(#httpd{method='GET'}=Req, DocumentRoot) -> - "/" ++ UrlPath = couch_httpd:path(Req), - case couch_httpd:partition(UrlPath) of - {_ActionKey, "/", RelativePath} -> - % GET /_utils/path or GET /_utils/ - CachingHeaders = - [{"Cache-Control", "private, must-revalidate"}], - couch_httpd:serve_file(Req, RelativePath, DocumentRoot, CachingHeaders); - {_ActionKey, "", _RelativePath} -> - % GET /_utils - RedirectPath = couch_httpd:path(Req) ++ "/", - couch_httpd:send_redirect(Req, RedirectPath) - end; -handle_utils_dir_req(Req, _) -> - send_method_not_allowed(Req, "GET,HEAD"). - -handle_all_dbs_req(#httpd{method='GET'}=Req) -> - {ok, DbNames} = couch_server:all_databases(), - send_json(Req, DbNames); -handle_all_dbs_req(Req) -> - send_method_not_allowed(Req, "GET,HEAD"). - - -handle_task_status_req(#httpd{method='GET'}=Req) -> - ok = couch_httpd:verify_is_server_admin(Req), - % convert the list of prop lists to a list of json objects - send_json(Req, [{Props} || Props <- couch_task_status:all()]); -handle_task_status_req(Req) -> - send_method_not_allowed(Req, "GET,HEAD"). - - -handle_restart_req(#httpd{method='POST'}=Req) -> - couch_httpd:validate_ctype(Req, "application/json"), - ok = couch_httpd:verify_is_server_admin(Req), - Result = send_json(Req, 202, {[{ok, true}]}), - couch_server_sup:restart_core_server(), - Result; -handle_restart_req(Req) -> - send_method_not_allowed(Req, "POST"). - - -handle_uuids_req(#httpd{method='GET'}=Req) -> - Count = list_to_integer(couch_httpd:qs_value(Req, "count", "1")), - UUIDs = [couch_uuids:new() || _ <- lists:seq(1, Count)], - Etag = couch_httpd:make_etag(UUIDs), - couch_httpd:etag_respond(Req, Etag, fun() -> - CacheBustingHeaders = [ - {"Date", couch_util:rfc1123_date()}, - {"Cache-Control", "no-cache"}, - % Past date, ON PURPOSE! - {"Expires", "Fri, 01 Jan 1990 00:00:00 GMT"}, - {"Pragma", "no-cache"}, - {"ETag", Etag} - ], - send_json(Req, 200, CacheBustingHeaders, {[{<<"uuids">>, UUIDs}]}) - end); -handle_uuids_req(Req) -> - send_method_not_allowed(Req, "GET"). - - -% Config request handler - - -% GET /_config/ -% GET /_config -handle_config_req(#httpd{method='GET', path_parts=[_]}=Req) -> - ok = couch_httpd:verify_is_server_admin(Req), - Grouped = lists:foldl(fun({{Section, Key}, Value}, Acc) -> - case dict:is_key(Section, Acc) of - true -> - dict:append(Section, {list_to_binary(Key), list_to_binary(Value)}, Acc); - false -> - dict:store(Section, [{list_to_binary(Key), list_to_binary(Value)}], Acc) - end - end, dict:new(), couch_config:all()), - KVs = dict:fold(fun(Section, Values, Acc) -> - [{list_to_binary(Section), {Values}} | Acc] - end, [], Grouped), - send_json(Req, 200, {KVs}); -% GET /_config/Section -handle_config_req(#httpd{method='GET', path_parts=[_,Section]}=Req) -> - ok = couch_httpd:verify_is_server_admin(Req), - KVs = [{list_to_binary(Key), list_to_binary(Value)} - || {Key, Value} <- couch_config:get(Section)], - send_json(Req, 200, {KVs}); -% GET /_config/Section/Key -handle_config_req(#httpd{method='GET', path_parts=[_, Section, Key]}=Req) -> - ok = couch_httpd:verify_is_server_admin(Req), - case couch_config:get(Section, Key, null) of - null -> - throw({not_found, unknown_config_value}); - Value -> - send_json(Req, 200, list_to_binary(Value)) - end; -% PUT or DELETE /_config/Section/Key -handle_config_req(#httpd{method=Method, path_parts=[_, Section, Key]}=Req) - when (Method == 'PUT') or (Method == 'DELETE') -> - ok = couch_httpd:verify_is_server_admin(Req), - Persist = couch_httpd:header_value(Req, "X-Couch-Persist") /= "false", - case couch_config:get(<<"httpd">>, <<"config_whitelist">>, null) of - null -> - % No whitelist; allow all changes. - handle_approved_config_req(Req, Persist); - WhitelistValue -> - % Provide a failsafe to protect against inadvertently locking - % onesself out of the config by supplying a syntactically-incorrect - % Erlang term. To intentionally lock down the whitelist, supply a - % well-formed list which does not include the whitelist config - % variable itself. - FallbackWhitelist = [{<<"httpd">>, <<"config_whitelist">>}], - - Whitelist = case couch_util:parse_term(WhitelistValue) of - {ok, Value} when is_list(Value) -> - Value; - {ok, _NonListValue} -> - FallbackWhitelist; - {error, _} -> - [{WhitelistSection, WhitelistKey}] = FallbackWhitelist, - ?LOG_ERROR("Only whitelisting ~s/~s due to error parsing: ~p", - [WhitelistSection, WhitelistKey, WhitelistValue]), - FallbackWhitelist - end, - - IsRequestedKeyVal = fun(Element) -> - case Element of - {A, B} -> - % For readability, tuples may be used instead of binaries - % in the whitelist. - case {couch_util:to_binary(A), couch_util:to_binary(B)} of - {Section, Key} -> - true; - {Section, <<"*">>} -> - true; - _Else -> - false - end; - _Else -> - false - end - end, - - case lists:any(IsRequestedKeyVal, Whitelist) of - true -> - % Allow modifying this whitelisted variable. - handle_approved_config_req(Req, Persist); - _NotWhitelisted -> - % Disallow modifying this non-whitelisted variable. - send_error(Req, 400, <<"modification_not_allowed">>, - ?l2b("This config variable is read-only")) - end - end; -handle_config_req(Req) -> - send_method_not_allowed(Req, "GET,PUT,DELETE"). - -% PUT /_config/Section/Key -% "value" -handle_approved_config_req(Req, Persist) -> - Query = couch_httpd:qs(Req), - UseRawValue = case lists:keyfind("raw", 1, Query) of - false -> false; % Not specified - {"raw", ""} -> false; % Specified with no value, i.e. "?raw" and "?raw=" - {"raw", "false"} -> false; - {"raw", "true"} -> true; - {"raw", InvalidValue} -> InvalidValue - end, - handle_approved_config_req(Req, Persist, UseRawValue). - -handle_approved_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req, - Persist, UseRawValue) - when UseRawValue =:= false orelse UseRawValue =:= true -> - RawValue = couch_httpd:json_body(Req), - Value = case UseRawValue of - true -> - % Client requests no change to the provided value. - RawValue; - false -> - % Pre-process the value as necessary. - case Section of - <<"admins">> -> - couch_passwords:hash_admin_password(RawValue); - _ -> - RawValue - end - end, - - OldValue = couch_config:get(Section, Key, ""), - case couch_config:set(Section, Key, ?b2l(Value), Persist) of - ok -> - send_json(Req, 200, list_to_binary(OldValue)); - Error -> - throw(Error) - end; - -handle_approved_config_req(#httpd{method='PUT'}=Req, _Persist, UseRawValue) -> - Err = io_lib:format("Bad value for 'raw' option: ~s", [UseRawValue]), - send_json(Req, 400, {[{error, ?l2b(Err)}]}); - -% DELETE /_config/Section/Key -handle_approved_config_req(#httpd{method='DELETE',path_parts=[_,Section,Key]}=Req, - Persist, _UseRawValue) -> - case couch_config:get(Section, Key, null) of - null -> - throw({not_found, unknown_config_value}); - OldValue -> - couch_config:delete(Section, Key, Persist), - send_json(Req, 200, list_to_binary(OldValue)) - end. - - -% httpd db handlers - -increment_update_seq_req(#httpd{method='POST'}=Req, Db) -> - couch_httpd:validate_ctype(Req, "application/json"), - {ok, NewSeq} = couch_db:increment_update_seq(Db), - send_json(Req, {[{ok, true}, - {update_seq, NewSeq} - ]}); -increment_update_seq_req(Req, _Db) -> - send_method_not_allowed(Req, "POST"). - -% httpd log handlers - -handle_log_req(#httpd{method='GET'}=Req) -> - ok = couch_httpd:verify_is_server_admin(Req), - Bytes = list_to_integer(couch_httpd:qs_value(Req, "bytes", "1000")), - Offset = list_to_integer(couch_httpd:qs_value(Req, "offset", "0")), - Chunk = couch_log:read(Bytes, Offset), - {ok, Resp} = start_chunked_response(Req, 200, [ - % send a plaintext response - {"Content-Type", "text/plain; charset=utf-8"}, - {"Content-Length", integer_to_list(length(Chunk))} - ]), - send_chunk(Resp, Chunk), - last_chunk(Resp); -handle_log_req(#httpd{method='POST'}=Req) -> - {PostBody} = couch_httpd:json_body_obj(Req), - Level = couch_util:get_value(<<"level">>, PostBody), - Message = ?b2l(couch_util:get_value(<<"message">>, PostBody)), - case Level of - <<"debug">> -> - ?LOG_DEBUG(Message, []), - send_json(Req, 200, {[{ok, true}]}); - <<"info">> -> - ?LOG_INFO(Message, []), - send_json(Req, 200, {[{ok, true}]}); - <<"error">> -> - ?LOG_ERROR(Message, []), - send_json(Req, 200, {[{ok, true}]}); - _ -> - send_json(Req, 400, {[{error, ?l2b(io_lib:format("Unrecognized log level '~s'", [Level]))}]}) - end; -handle_log_req(Req) -> - send_method_not_allowed(Req, "GET,POST"). http://git-wip-us.apache.org/repos/asf/couchdb/blob/5618e610/src/apps/couch/src/couch_httpd_oauth.erl ---------------------------------------------------------------------- diff --git a/src/apps/couch/src/couch_httpd_oauth.erl b/src/apps/couch/src/couch_httpd_oauth.erl deleted file mode 100644 index 2094c08..0000000 --- a/src/apps/couch/src/couch_httpd_oauth.erl +++ /dev/null @@ -1,387 +0,0 @@ -% Licensed under the Apache License, Version 2.0 (the "License"); you may not -% use this file except in compliance with the License. You may obtain a copy of -% the License at -% -% http://www.apache.org/licenses/LICENSE-2.0 -% -% Unless required by applicable law or agreed to in writing, software -% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -% License for the specific language governing permissions and limitations under -% the License. - --module(couch_httpd_oauth). - --include("couch_db.hrl"). --include("couch_js_functions.hrl"). - --export([oauth_authentication_handler/1, handle_oauth_req/1]). - --define(OAUTH_DDOC_ID, <<"_design/oauth">>). --define(OAUTH_VIEW_NAME, <<"oauth_credentials">>). - --record(callback_params, { - consumer, - token, - token_secret, - url, - signature, - params, - username -}). - -% OAuth auth handler using per-node user db -oauth_authentication_handler(Req) -> - serve_oauth(Req, fun oauth_auth_callback/2, true). - - -oauth_auth_callback(Req, #callback_params{token_secret = undefined}) -> - couch_httpd:send_error( - Req, 400, <<"invalid_token">>, <<"Invalid OAuth token.">>); - -oauth_auth_callback(#httpd{mochi_req = MochiReq} = Req, CbParams) -> - Method = atom_to_list(MochiReq:get(method)), - #callback_params{ - consumer = Consumer, - token_secret = TokenSecret, - url = Url, - signature = Sig, - params = Params, - username = User - } = CbParams, - case oauth:verify(Sig, Method, Url, Params, Consumer, TokenSecret) of - true -> - set_user_ctx(Req, User); - false -> - ?LOG_DEBUG("OAuth handler: signature verification failed for user `~p`~n" - "Received signature is `~p`~n" - "HTTP method is `~p`~n" - "URL is `~p`~n" - "Parameters are `~p`~n" - "Consumer is `~p`, token secret is `~p`~n" - "Expected signature was `~p`~n", - [User, Sig, Method, Url, Params, Consumer, TokenSecret, - oauth:signature(Method, Url, Params, Consumer, TokenSecret)]), - Req - end. - - -% Look up the consumer key and get the roles to give the consumer -set_user_ctx(_Req, undefined) -> - throw({bad_request, unknown_oauth_token}); -set_user_ctx(Req, Name) -> - case couch_auth_cache:get_user_creds(Name) of - nil -> - ?LOG_DEBUG("OAuth handler: user `~p` credentials not found", [Name]), - Req; - User -> - Roles = couch_util:get_value(<<"roles">>, User, []), - Req#httpd{user_ctx=#user_ctx{name=Name, roles=Roles}} - end. - -% OAuth request_token -handle_oauth_req(#httpd{path_parts=[_OAuth, <<"request_token">>], method=Method}=Req1) -> - serve_oauth(Req1, fun(Req, CbParams) -> - #callback_params{ - consumer = Consumer, - token_secret = TokenSecret, - url = Url, - signature = Sig, - params = Params - } = CbParams, - case oauth:verify( - Sig, atom_to_list(Method), Url, Params, Consumer, TokenSecret) of - true -> - ok(Req, <<"oauth_token=requestkey&oauth_token_secret=requestsecret">>); - false -> - invalid_signature(Req) - end - end, false); -handle_oauth_req(#httpd{path_parts=[_OAuth, <<"authorize">>]}=Req) -> - {ok, serve_oauth_authorize(Req)}; -handle_oauth_req(#httpd{path_parts=[_OAuth, <<"access_token">>], method='GET'}=Req1) -> - serve_oauth(Req1, fun(Req, CbParams) -> - #callback_params{ - consumer = Consumer, - token = Token, - url = Url, - signature = Sig, - params = Params - } = CbParams, - case Token of - "requestkey" -> - case oauth:verify( - Sig, "GET", Url, Params, Consumer, "requestsecret") of - true -> - ok(Req, - <<"oauth_token=accesskey&oauth_token_secret=accesssecret">>); - false -> - invalid_signature(Req) - end; - _ -> - couch_httpd:send_error( - Req, 400, <<"invalid_token">>, <<"Invalid OAuth token.">>) - end - end, false); -handle_oauth_req(#httpd{path_parts=[_OAuth, <<"access_token">>]}=Req) -> - couch_httpd:send_method_not_allowed(Req, "GET"). - -invalid_signature(Req) -> - couch_httpd:send_error(Req, 400, <<"invalid_signature">>, <<"Invalid signature value.">>). - -% This needs to be protected i.e. force user to login using HTTP Basic Auth or form-based login. -serve_oauth_authorize(#httpd{method=Method}=Req1) -> - case Method of - 'GET' -> - % Confirm with the User that they want to authenticate the Consumer - serve_oauth(Req1, fun(Req, CbParams) -> - #callback_params{ - consumer = Consumer, - token_secret = TokenSecret, - url = Url, - signature = Sig, - params = Params - } = CbParams, - case oauth:verify( - Sig, "GET", Url, Params, Consumer, TokenSecret) of - true -> - ok(Req, <<"oauth_token=requestkey&", - "oauth_token_secret=requestsecret">>); - false -> - invalid_signature(Req) - end - end, false); - 'POST' -> - % If the User has confirmed, we direct the User back to the Consumer with a verification code - serve_oauth(Req1, fun(Req, CbParams) -> - #callback_params{ - consumer = Consumer, - token_secret = TokenSecret, - url = Url, - signature = Sig, - params = Params - } = CbParams, - case oauth:verify( - Sig, "POST", Url, Params, Consumer, TokenSecret) of - true -> - %redirect(oauth_callback, oauth_token, oauth_verifier), - ok(Req, <<"oauth_token=requestkey&", - "oauth_token_secret=requestsecret">>); - false -> - invalid_signature(Req) - end - end, false); - _ -> - couch_httpd:send_method_not_allowed(Req1, "GET,POST") - end. - -serve_oauth(#httpd{mochi_req=MochiReq}=Req, Fun, FailSilently) -> - % 1. In the HTTP Authorization header as defined in OAuth HTTP Authorization Scheme. - % 2. As the HTTP POST request body with a content-type of application/x-www-form-urlencoded. - % 3. Added to the URLs in the query part (as defined by [RFC3986] section 3). - AuthHeader = case MochiReq:get_header_value("authorization") of - undefined -> - ""; - Else -> - [Head | Tail] = re:split(Else, "\\s", [{parts, 2}, {return, list}]), - case [string:to_lower(Head) | Tail] of - ["oauth", Rest] -> Rest; - _ -> "" - end - end, - HeaderParams = oauth:header_params_decode(AuthHeader), - %Realm = couch_util:get_value("realm", HeaderParams), - - % get requested path - RequestedPath = case MochiReq:get_header_value("x-couchdb-requested-path") of - undefined -> - case MochiReq:get_header_value("x-couchdb-vhost-path") of - undefined -> - MochiReq:get(raw_path); - VHostPath -> - VHostPath - end; - RequestedPath0 -> - RequestedPath0 - end, - {_, QueryString, _} = mochiweb_util:urlsplit_path(RequestedPath), - - Params = proplists:delete("realm", HeaderParams) ++ mochiweb_util:parse_qs(QueryString), - - ?LOG_DEBUG("OAuth Params: ~p", [Params]), - case couch_util:get_value("oauth_version", Params, "1.0") of - "1.0" -> - case couch_util:get_value("oauth_consumer_key", Params, undefined) of - undefined -> - case FailSilently of - true -> Req; - false -> couch_httpd:send_error(Req, 400, <<"invalid_consumer">>, <<"Invalid consumer.">>) - end; - ConsumerKey -> - Url = couch_httpd:absolute_uri(Req, RequestedPath), - case get_callback_params(ConsumerKey, Params, Url) of - {ok, CallbackParams} -> - Fun(Req, CallbackParams); - invalid_consumer_token_pair -> - couch_httpd:send_error( - Req, 400, - <<"invalid_consumer_token_pair">>, - <<"Invalid consumer and token pair.">>); - {error, {Error, Reason}} -> - couch_httpd:send_error(Req, 400, Error, Reason) - end - end; - _ -> - couch_httpd:send_error(Req, 400, <<"invalid_oauth_version">>, <<"Invalid OAuth version.">>) - end. - - -get_callback_params(ConsumerKey, Params, Url) -> - Token = couch_util:get_value("oauth_token", Params), - SigMethod = sig_method(Params), - CbParams0 = #callback_params{ - token = Token, - signature = couch_util:get_value("oauth_signature", Params), - params = proplists:delete("oauth_signature", Params), - url = Url - }, - case oauth_credentials_info(Token, ConsumerKey) of - nil -> - invalid_consumer_token_pair; - {error, _} = Err -> - Err; - {OauthCreds} -> - User = couch_util:get_value(<<"username">>, OauthCreds, []), - ConsumerSecret = ?b2l(couch_util:get_value( - <<"consumer_secret">>, OauthCreds, <<>>)), - TokenSecret = ?b2l(couch_util:get_value( - <<"token_secret">>, OauthCreds, <<>>)), - case (User =:= []) orelse (ConsumerSecret =:= []) orelse - (TokenSecret =:= []) of - true -> - invalid_consumer_token_pair; - false -> - CbParams = CbParams0#callback_params{ - consumer = {ConsumerKey, ConsumerSecret, SigMethod}, - token_secret = TokenSecret, - username = User - }, - ?LOG_DEBUG("Got OAuth credentials, for ConsumerKey `~p` and " - "Token `~p`, from the views, User: `~p`, " - "ConsumerSecret: `~p`, TokenSecret: `~p`", - [ConsumerKey, Token, User, ConsumerSecret, TokenSecret]), - {ok, CbParams} - end - end. - - -sig_method(Params) -> - sig_method_1(couch_util:get_value("oauth_signature_method", Params)). -sig_method_1("PLAINTEXT") -> - plaintext; -% sig_method_1("RSA-SHA1") -> -% rsa_sha1; -sig_method_1("HMAC-SHA1") -> - hmac_sha1; -sig_method_1(_) -> - undefined. - - -ok(#httpd{mochi_req=MochiReq}, Body) -> - {ok, MochiReq:respond({200, [], Body})}. - - -oauth_credentials_info(Token, ConsumerKey) -> - case use_auth_db() of - {ok, Db} -> - Result = case query_oauth_view(Db, [?l2b(ConsumerKey), ?l2b(Token)]) of - [] -> - nil; - [Creds] -> - Creds; - [_ | _] -> - Reason = iolist_to_binary( - io_lib:format("Found multiple OAuth credentials for the pair " - " (consumer_key: `~p`, token: `~p`)", [ConsumerKey, Token])), - {error, {<<"oauth_token_consumer_key_pair">>, Reason}} - end, - couch_db:close(Db), - Result; - nil -> - { - case couch_config:get("oauth_consumer_secrets", ConsumerKey) of - undefined -> []; - ConsumerSecret -> [{<<"consumer_secret">>, ?l2b(ConsumerSecret)}] - end - ++ - case couch_config:get("oauth_token_secrets", Token) of - undefined -> []; - TokenSecret -> [{<<"token_secret">>, ?l2b(TokenSecret)}] - end - ++ - case couch_config:get("oauth_token_users", Token) of - undefined -> []; - User -> [{<<"username">>, ?l2b(User)}] - end - } - end. - - -use_auth_db() -> - case couch_config:get("couch_httpd_oauth", "use_users_db", "false") of - "false" -> - nil; - "true" -> - AuthDb = open_auth_db(), - {ok, _AuthDb2} = ensure_oauth_views_exist(AuthDb) - end. - - -open_auth_db() -> - DbName = ?l2b(couch_config:get("couch_httpd_auth", "authentication_db")), - DbOptions = [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}], - {ok, AuthDb} = couch_db:open_int(DbName, DbOptions), - AuthDb. - - -ensure_oauth_views_exist(AuthDb) -> - case couch_db:open_doc(AuthDb, ?OAUTH_DDOC_ID, []) of - {ok, _DDoc} -> - {ok, AuthDb}; - _ -> - {ok, DDoc} = get_oauth_ddoc(), - {ok, _Rev} = couch_db:update_doc(AuthDb, DDoc, []), - {ok, _AuthDb2} = couch_db:reopen(AuthDb) - end. - - -get_oauth_ddoc() -> - Json = {[ - {<<"_id">>, ?OAUTH_DDOC_ID}, - {<<"language">>, <<"javascript">>}, - {<<"views">>, - {[ - {?OAUTH_VIEW_NAME, - {[ - {<<"map">>, ?OAUTH_MAP_FUN} - ]} - } - ]} - } - ]}, - {ok, couch_doc:from_json_obj(Json)}. - - -query_oauth_view(Db, Key) -> - ViewOptions = [ - {start_key, Key}, - {end_key, Key} - ], - Callback = fun({row, Row}, Acc) -> - {ok, [couch_util:get_value(value, Row) | Acc]}; - (_, Acc) -> - {ok, Acc} - end, - {ok, Result} = couch_mrview:query_view( - Db, ?OAUTH_DDOC_ID, ?OAUTH_VIEW_NAME, ViewOptions, Callback, []), - Result.