couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From rnew...@apache.org
Subject [3/6] Merge remote-tracking branch 'origin/import-master'
Date Tue, 06 May 2014 12:40:58 GMT
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/9d629ff6/src/couch_httpd_db.erl
----------------------------------------------------------------------
diff --cc src/couch_httpd_db.erl
index 50fba6c,0000000..8c7ae77
mode 100644,000000..100644
--- a/src/couch_httpd_db.erl
+++ b/src/couch_httpd_db.erl
@@@ -1,1210 -1,0 +1,1230 @@@
 +% 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_lib("couch/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(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),
 +    Options = case couch_httpd:qs_value(Req, "sync") of
 +        "true" -> [sync, {user_ctx, UserCtx}];
 +        _ -> [{user_ctx, UserCtx}]
 +    end,
 +    case couch_server:delete(DbName, Options) 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">>, ContentType},
++            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 = make_content_range(From, To, Len),
++        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) ->
-     ?l2b(io_lib:format("bytes ~B-~B/~B", [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)}]
++                            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) ->
-     lists:foldl(fun({Key, Value}, Args) ->
++    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)).
++    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-couch/blob/9d629ff6/src/couch_httpd_external.erl
----------------------------------------------------------------------
diff --cc src/couch_httpd_external.erl
index 8322dcd,0000000..d1a5d01
mode 100644,000000..100644
--- a/src/couch_httpd_external.erl
+++ b/src/couch_httpd_external.erl
@@@ -1,173 -1,0 +1,177 @@@
 +% 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_lib("couch/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(
 +                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(#httpd{mochi_req=MochiReq}=Req, Response) ->
++send_external_response(Req, Response) ->
 +    #extern_resp_args{
 +        code = Code,
 +        data = Data,
 +        ctype = CType,
-         headers = Headers
++        headers = Headers,
++        json = Json
 +    } = parse_external_response(Response),
-     couch_httpd:log_request(Req, Code),
-     Resp = MochiReq:respond({Code,
-         default_or_content_type(CType, Headers ++ couch_httpd:server_header()), Data}),
-     {ok, Resp}.
++    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{
-                     data=?JSON_ENCODE(Value),
++                    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-couch/blob/9d629ff6/src/couch_httpd_misc_handlers.erl
----------------------------------------------------------------------
diff --cc src/couch_httpd_misc_handlers.erl
index 4cf16f6,0000000..9dc89ec
mode 100644,000000..100644
--- a/src/couch_httpd_misc_handlers.erl
+++ b/src/couch_httpd_misc_handlers.erl
@@@ -1,272 -1,0 +1,299 @@@
 +% 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_task_status_req/1, handle_file_req/2]).
 +
 +-export([increment_update_seq_req/2]).
 +
 +
 +-include_lib("couch/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 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='GET', path_parts=[_, <<"token">>]}=Req) ->
 +    ok = couch_httpd:verify_is_server_admin(Req),
 +    Token = case application:get_env(couch, instance_token) of
 +        {ok, Tok} ->
 +            Tok;
 +        _ ->
 +            Tok = erlang:phash2(make_ref()),
 +            application:set_env(couch, instance_token, Tok),
 +            Tok
 +    end,
 +    send_json(Req, 200, {[{token, Token}]});
 +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:restart(),
 +    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(), 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} <- 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 config:get(Section, Key, null) of
 +    null ->
 +        throw({not_found, unknown_config_value});
 +    Value ->
 +        send_json(Req, 200, list_to_binary(Value))
 +    end;
 +% POST /_config/_reload - Flushes unpersisted config values from RAM
 +handle_config_req(#httpd{method='POST', path_parts=[_, <<"_reload">>]}=Req) ->
 +    ok = couch_httpd:verify_is_server_admin(Req),
 +    ok = config:reload(),
 +    send_json(Req, 200, {[{ok, true}]});
 +% 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 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,POST,DELETE").
 +
 +% PUT /_config/Section/Key
 +% "value"
- handle_approved_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req, Persist) ->
-     Value = case Section of
-     <<"admins">> ->
-         couch_passwords:hash_admin_password(couch_httpd:json_body(Req));
-     _ ->
-         couch_httpd:json_body(Req)
++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 = config:get(Section, Key, ""),
 +    case 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) ->
++handle_approved_config_req(#httpd{method='DELETE',path_parts=[_,Section,Key]}=Req,
++                           Persist, _UseRawValue) ->
 +    case config:get(Section, Key, null) of
 +    null ->
 +        throw({not_found, unknown_config_value});
 +    OldValue ->
 +        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").

http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/9d629ff6/src/couch_httpd_oauth.erl
----------------------------------------------------------------------
diff --cc src/couch_httpd_oauth.erl
index 8fd4c30,0000000..15733da
mode 100644,000000..100644
--- a/src/couch_httpd_oauth.erl
+++ b/src/couch_httpd_oauth.erl
@@@ -1,387 -1,0 +1,388 @@@
 +% 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_lib("couch/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 = Token,
 +        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:sign(Method, Url, Params, Consumer, "", TokenSecret)]),
++                oauth:sign(Method, Url, Params, Consumer, Token, 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 config:get("oauth_consumer_secrets", ConsumerKey) of
 +            undefined -> [];
 +            ConsumerSecret -> [{<<"consumer_secret">>, ?l2b(ConsumerSecret)}]
 +            end
 +            ++
 +            case config:get("oauth_token_secrets", Token) of
 +            undefined -> [];
 +            TokenSecret -> [{<<"token_secret">>, ?l2b(TokenSecret)}]
 +            end
 +            ++
 +            case config:get("oauth_token_users", Token) of
 +            undefined -> [];
 +            User -> [{<<"username">>, ?l2b(User)}]
 +            end
 +        }
 +    end.
 +
 +
 +use_auth_db() ->
 +    case 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(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.


Mime
View raw message