couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From rnew...@apache.org
Subject [4/6] Merge remote-tracking branch 'origin/import-master'
Date Tue, 06 May 2014 12:40:59 GMT
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/9d629ff6/src/couch_httpd.erl
----------------------------------------------------------------------
diff --cc src/couch_httpd.erl
index 7028d70,0000000..a564d75
mode 100644,000000..100644
--- a/src/couch_httpd.erl
+++ b/src/couch_httpd.erl
@@@ -1,1087 -1,0 +1,1052 @@@
 +% 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).
 +-include_lib("couch/include/couch_db.hrl").
 +
 +-export([start_link/0, start_link/1, stop/0, handle_request/5]).
 +
 +-export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,qs_json_value/3]).
 +-export([path/1,absolute_uri/2,body_length/1]).
 +-export([verify_is_server_admin/1,unquote/1,quote/1,recv/2,recv_chunked/4,error_info/1]).
 +-export([make_fun_spec_strs/1]).
 +-export([make_arity_1_fun/1, make_arity_2_fun/1, make_arity_3_fun/1]).
 +-export([parse_form/1,json_body/1,json_body_obj/1,body/1]).
 +-export([doc_etag/1, make_etag/1, etag_match/2, etag_respond/3, etag_maybe/2]).
 +-export([primary_header_value/2,partition/1,serve_file/3,serve_file/4, server_header/0]).
 +-export([start_chunked_response/3,send_chunk/2,log_request/2]).
 +-export([start_response_length/4, start_response/3, send/2]).
 +-export([start_json_response/2, start_json_response/3, end_json_response/1]).
- -export([send_response/4,send_method_not_allowed/2,send_error/4, send_redirect/2,send_chunked_error/2]).
++-export([send_response/4,send_method_not_allowed/2,send_error/2,send_error/4, send_redirect/2,send_chunked_error/2]).
 +-export([send_json/2,send_json/3,send_json/4,last_chunk/1,parse_multipart_request/3]).
 +-export([accepted_encodings/1,handle_request_int/5,validate_referer/1,validate_ctype/2]).
 +-export([http_1_0_keep_alive/2]).
 +
 +start_link() ->
 +    start_link(http).
 +start_link(http) ->
 +    Port = config:get("httpd", "port", "5984"),
 +    start_link(?MODULE, [{port, Port}]);
 +start_link(https) ->
 +    Port = config:get("ssl", "port", "6984"),
-     CertFile = config:get("ssl", "cert_file", nil),
-     KeyFile = config:get("ssl", "key_file", nil),
-     Options = case CertFile /= nil andalso KeyFile /= nil of
++    ServerOpts0 =
++        [{cacertfile, config:get("ssl", "cacert_file", nil)},
++         {keyfile, config:get("ssl", "key_file", nil)},
++         {certfile, onfig:get("ssl", "cert_file", nil)},
++         {password, config:get("ssl", "password", nil)}],
++
++    case (couch_util:get_value(keyfile, ServerOpts0) == nil orelse
++        couch_util:get_value(certfile, ServerOpts0) == nil) of
 +        true ->
-             SslOpts = [{certfile, CertFile}, {keyfile, KeyFile}],
- 
-             %% set password if one is needed for the cert
-             SslOpts1 = case config:get("ssl", "password", nil) of
-                 nil -> SslOpts;
-                 Password ->
-                     SslOpts ++ [{password, Password}]
-             end,
-             % do we verify certificates ?
-             FinalSslOpts = case config:get("ssl",
-                     "verify_ssl_certificates", "false") of
-                 "false" -> SslOpts1;
-                 "true" ->
-                     case config:get("ssl",
-                             "cacert_file", nil) of
-                         nil ->
-                             io:format("Verify SSL certificate "
-                                 ++"enabled but file containing "
-                                 ++"PEM encoded CA certificates is "
-                                 ++"missing", []),
-                             throw({error, missing_cacerts});
-                         CaCertFile ->
-                             Depth = list_to_integer(config:get("ssl",
-                                     "ssl_certificate_max_depth",
-                                     "1")),
-                             FinalOpts = [
-                                 {cacertfile, CaCertFile},
-                                 {depth, Depth},
-                                 {verify, verify_peer}],
-                             % allows custom verify fun.
-                             case config:get("ssl",
-                                     "verify_fun", nil) of
-                                 nil -> FinalOpts;
-                                 SpecStr ->
-                                     FinalOpts
-                                     ++ [{verify_fun, make_arity_3_fun(SpecStr)}]
-                             end
-                     end
-             end,
- 
-             [{port, Port},
-                 {ssl, true},
-                 {ssl_opts, FinalSslOpts}];
-         false ->
 +            io:format("SSL enabled but PEM certificates are missing.", []),
-             throw({error, missing_certs})
++            throw({error, missing_certs});
++        false ->
++            ok
++    end,
++
++    ServerOpts = [Opt || {_, V}=Opt <- ServerOpts0, V /= nil],
++
++    ClientOpts = case config:get("ssl", "verify_ssl_certificates", "false") of
++        "false" ->
++            [];
++        "true" ->
++            [{depth, list_to_integer(config:get("ssl",
++                "ssl_certificate_max_depth", "1"))},
++             {verify, verify_peer}] ++
++            case config:get("ssl", "verify_fun", nil) of
++                nil -> [];
++                SpecStr ->
++                    [{verify_fun, make_arity_3_fun(SpecStr)}]
++            end
 +    end,
++    SslOpts = ServerOpts ++ ClientOpts,
++
++    Options =
++        [{port, Port},
++         {ssl, true},
++         {ssl_opts, SslOpts}],
 +    start_link(https, Options).
 +start_link(Name, Options) ->
 +    BindAddress = config:get("httpd", "bind_address", any),
 +    validate_bind_address(BindAddress),
 +    DefaultSpec = "{couch_httpd_db, handle_request}",
 +    DefaultFun = make_arity_1_fun(
 +        config:get("httpd", "default_handler", DefaultSpec)
 +    ),
 +
 +    UrlHandlersList = lists:map(
 +        fun({UrlKey, SpecStr}) ->
 +            {?l2b(UrlKey), make_arity_1_fun(SpecStr)}
 +        end, config:get("httpd_global_handlers")),
 +
 +    DbUrlHandlersList = lists:map(
 +        fun({UrlKey, SpecStr}) ->
 +            {?l2b(UrlKey), make_arity_2_fun(SpecStr)}
 +        end, config:get("httpd_db_handlers")),
 +
 +    DesignUrlHandlersList = lists:map(
 +        fun({UrlKey, SpecStr}) ->
 +            {?l2b(UrlKey), make_arity_3_fun(SpecStr)}
 +        end, config:get("httpd_design_handlers")),
 +
 +    UrlHandlers = dict:from_list(UrlHandlersList),
 +    DbUrlHandlers = dict:from_list(DbUrlHandlersList),
 +    DesignUrlHandlers = dict:from_list(DesignUrlHandlersList),
 +    {ok, ServerOptions} = couch_util:parse_term(
 +        config:get("httpd", "server_options", "[]")),
 +    {ok, SocketOptions} = couch_util:parse_term(
 +        config:get("httpd", "socket_options", "[]")),
 +
 +    set_auth_handlers(),
 +
 +    % ensure uuid is set so that concurrent replications
 +    % get the same value.
 +    couch_server:get_uuid(),
 +
 +    Loop = fun(Req)->
 +        case SocketOptions of
 +        [] ->
 +            ok;
 +        _ ->
 +            ok = mochiweb_socket:setopts(Req:get(socket), SocketOptions)
 +        end,
 +        apply(?MODULE, handle_request, [
 +            Req, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers
 +        ])
 +    end,
 +
 +    % set mochiweb options
 +    FinalOptions = lists:append([Options, ServerOptions, [
 +            {loop, Loop},
 +            {name, Name},
 +            {ip, BindAddress}]]),
 +
 +    % launch mochiweb
 +    case mochiweb_http:start(FinalOptions) of
 +        {ok, MochiPid} ->
 +            {ok, MochiPid};
 +        {error, Reason} ->
 +            io:format("Failure to start Mochiweb: ~s~n",[Reason]),
 +            throw({error, Reason})
 +    end.
 +
 +
 +stop() ->
 +    mochiweb_http:stop(couch_httpd),
 +    mochiweb_http:stop(https).
 +
 +
 +set_auth_handlers() ->
 +    AuthenticationSrcs = make_fun_spec_strs(
 +        config:get("httpd", "authentication_handlers", "")),
 +    AuthHandlers = lists:map(
 +        fun(A) -> {make_arity_1_fun(A), ?l2b(A)} end, AuthenticationSrcs),
 +    ok = application:set_env(couch, auth_handlers, AuthHandlers).
 +
 +% SpecStr is a string like "{my_module, my_fun}"
 +%  or "{my_module, my_fun, <<"my_arg">>}"
 +make_arity_1_fun(SpecStr) ->
 +    case couch_util:parse_term(SpecStr) of
 +    {ok, {Mod, Fun, SpecArg}} ->
 +        fun(Arg) -> Mod:Fun(Arg, SpecArg) end;
 +    {ok, {Mod, Fun}} ->
 +        fun(Arg) -> Mod:Fun(Arg) end
 +    end.
 +
 +make_arity_2_fun(SpecStr) ->
 +    case couch_util:parse_term(SpecStr) of
 +    {ok, {Mod, Fun, SpecArg}} ->
 +        fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2, SpecArg) end;
 +    {ok, {Mod, Fun}} ->
 +        fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2) end
 +    end.
 +
 +make_arity_3_fun(SpecStr) ->
 +    case couch_util:parse_term(SpecStr) of
 +    {ok, {Mod, Fun, SpecArg}} ->
 +        fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3, SpecArg) end;
 +    {ok, {Mod, Fun}} ->
 +        fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3) end
 +    end.
 +
 +% SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}"
 +make_fun_spec_strs(SpecStr) ->
 +    re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}]).
 +
 +handle_request(MochiReq, DefaultFun, UrlHandlers, DbUrlHandlers,
 +    DesignUrlHandlers) ->
++    %% reset rewrite count for new request
++    erlang:put(?REWRITE_COUNT, 0),
 +
 +    MochiReq1 = couch_httpd_vhost:dispatch_host(MochiReq),
 +
 +    handle_request_int(MochiReq1, DefaultFun,
 +                UrlHandlers, DbUrlHandlers, DesignUrlHandlers).
 +
 +handle_request_int(MochiReq, DefaultFun,
 +            UrlHandlers, DbUrlHandlers, DesignUrlHandlers) ->
 +    Begin = now(),
 +    % for the path, use the raw path with the query string and fragment
 +    % removed, but URL quoting left intact
 +    RawUri = MochiReq:get(raw_path),
 +    {"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri),
 +
 +    Headers = MochiReq:get(headers),
 +
 +    % get requested path
 +    RequestedPath = case MochiReq:get_header_value("x-couchdb-vhost-path") of
 +        undefined ->
 +            case MochiReq:get_header_value("x-couchdb-requested-path") of
 +                undefined -> RawUri;
 +                R -> R
 +            end;
 +        P -> P
 +    end,
 +
 +    HandlerKey =
 +    case mochiweb_util:partition(Path, "/") of
 +    {"", "", ""} ->
 +        <<"/">>; % Special case the root url handler
 +    {FirstPart, _, _} ->
 +        list_to_binary(FirstPart)
 +    end,
 +    ?LOG_DEBUG("~p ~s ~p from ~p~nHeaders: ~p", [
 +        MochiReq:get(method),
 +        RawUri,
 +        MochiReq:get(version),
 +        MochiReq:get(peer),
 +        mochiweb_headers:to_list(MochiReq:get(headers))
 +    ]),
 +
 +    Method1 =
 +    case MochiReq:get(method) of
 +        % already an atom
 +        Meth when is_atom(Meth) -> Meth;
 +
 +        % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when
 +        % possible (if any module references the atom, then it's existing).
 +        Meth -> couch_util:to_existing_atom(Meth)
 +    end,
 +    increment_method_stats(Method1),
 +
 +    % allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE header
 +    MethodOverride = MochiReq:get_primary_header_value("X-HTTP-Method-Override"),
 +    Method2 = case lists:member(MethodOverride, ["GET", "HEAD", "POST",
 +                                                 "PUT", "DELETE",
 +                                                 "TRACE", "CONNECT",
 +                                                 "COPY"]) of
 +    true ->
 +        ?LOG_INFO("MethodOverride: ~s (real method was ~s)", [MethodOverride, Method1]),
 +        case Method1 of
 +        'POST' -> couch_util:to_existing_atom(MethodOverride);
 +        _ ->
 +            % Ignore X-HTTP-Method-Override when the original verb isn't POST.
 +            % I'd like to send a 406 error to the client, but that'd require a nasty refactor.
 +            % throw({not_acceptable, <<"X-HTTP-Method-Override may only be used with POST requests.">>})
 +            Method1
 +        end;
 +    _ -> Method1
 +    end,
 +
 +    % alias HEAD to GET as mochiweb takes care of stripping the body
 +    Method = case Method2 of
 +        'HEAD' -> 'GET';
 +        Other -> Other
 +    end,
 +
 +    HttpReq = #httpd{
 +        mochi_req = MochiReq,
 +        peer = MochiReq:get(peer),
 +        method = Method,
 +        requested_path_parts =
 +            [?l2b(unquote(Part)) || Part <- string:tokens(RequestedPath, "/")],
 +        path_parts = [?l2b(unquote(Part)) || Part <- string:tokens(Path, "/")],
 +        db_url_handlers = DbUrlHandlers,
 +        design_url_handlers = DesignUrlHandlers,
 +        default_fun = DefaultFun,
 +        url_handlers = UrlHandlers,
-         user_ctx = erlang:erase(pre_rewrite_user_ctx)
++        user_ctx = erlang:erase(pre_rewrite_user_ctx),
++        auth = erlang:erase(pre_rewrite_auth)
 +    },
 +
 +    HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun),
 +    {ok, AuthHandlers} = application:get_env(couch, auth_handlers),
 +
 +    {ok, Resp} =
 +    try
 +        case couch_httpd_cors:is_preflight_request(HttpReq) of
 +        #httpd{} ->
 +            case authenticate_request(HttpReq, AuthHandlers) of
 +            #httpd{} = Req ->
 +                HandlerFun(Req);
 +            Response ->
 +                Response
 +            end;
 +        Response ->
 +            Response
 +        end
 +    catch
 +        throw:{http_head_abort, Resp0} ->
 +            {ok, Resp0};
 +        throw:{invalid_json, S} ->
 +            ?LOG_ERROR("attempted upload of invalid JSON (set log_level to debug to log it)", []),
 +            ?LOG_DEBUG("Invalid JSON: ~p",[S]),
 +            send_error(HttpReq, {bad_request, invalid_json});
 +        throw:unacceptable_encoding ->
 +            ?LOG_ERROR("unsupported encoding method for the response", []),
 +            send_error(HttpReq, {not_acceptable, "unsupported encoding"});
 +        throw:bad_accept_encoding_value ->
 +            ?LOG_ERROR("received invalid Accept-Encoding header", []),
 +            send_error(HttpReq, bad_request);
 +        exit:normal ->
 +            exit(normal);
 +        exit:snappy_nif_not_loaded ->
 +            ErrorReason = "To access the database or view index, Apache CouchDB"
 +                " must be built with Erlang OTP R13B04 or higher.",
 +            ?LOG_ERROR("~s", [ErrorReason]),
 +            send_error(HttpReq, {bad_otp_release, ErrorReason});
 +        exit:{body_too_large, _} ->
 +            send_error(HttpReq, request_entity_too_large);
 +        throw:Error ->
 +            Stack = erlang:get_stacktrace(),
 +            ?LOG_DEBUG("Minor error in HTTP request: ~p",[Error]),
 +            ?LOG_DEBUG("Stacktrace: ~p",[Stack]),
 +            send_error(HttpReq, Error);
 +        error:badarg ->
 +            Stack = erlang:get_stacktrace(),
 +            ?LOG_ERROR("Badarg error in HTTP request",[]),
 +            ?LOG_INFO("Stacktrace: ~p",[Stack]),
 +            send_error(HttpReq, badarg);
 +        error:function_clause ->
 +            Stack = erlang:get_stacktrace(),
 +            ?LOG_ERROR("function_clause error in HTTP request",[]),
 +            ?LOG_INFO("Stacktrace: ~p",[Stack]),
 +            send_error(HttpReq, function_clause);
 +        Tag:Error ->
 +            Stack = erlang:get_stacktrace(),
 +            ?LOG_ERROR("Uncaught error in HTTP request: ~p",[{Tag, Error}]),
 +            ?LOG_INFO("Stacktrace: ~p",[Stack]),
 +            send_error(HttpReq, Error)
 +    end,
 +    RequestTime = round(timer:now_diff(now(), Begin)/1000),
 +    couch_stats_collector:record({couchdb, request_time}, RequestTime),
 +    couch_stats_collector:increment({httpd, requests}),
 +    {ok, Resp}.
 +
 +% Try authentication handlers in order until one sets a user_ctx
 +% the auth funs also have the option of returning a response
 +% move this to couch_httpd_auth?
 +authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthHandlers) ->
 +    Req;
 +authenticate_request(#httpd{} = Req, []) ->
 +    case config:get("couch_httpd_auth", "require_valid_user", "false") of
 +    "true" ->
 +        throw({unauthorized, <<"Authentication required.">>});
 +    "false" ->
 +        Req#httpd{user_ctx=#user_ctx{}}
 +    end;
 +authenticate_request(#httpd{} = Req, [{AuthFun, AuthSrc} | RestAuthHandlers]) ->
 +    R = case AuthFun(Req) of
 +        #httpd{user_ctx=#user_ctx{}=UserCtx}=Req2 ->
 +            Req2#httpd{user_ctx=UserCtx#user_ctx{handler=AuthSrc}};
 +        Else -> Else
 +    end,
 +    authenticate_request(R, RestAuthHandlers);
 +authenticate_request(Response, _AuthSrcs) ->
 +    Response.
 +
 +increment_method_stats(Method) ->
 +    couch_stats_collector:increment({httpd_request_methods, Method}).
 +
 +validate_referer(Req) ->
 +    Host = host_for_request(Req),
 +    Referer = header_value(Req, "Referer", fail),
 +    case Referer of
 +    fail ->
 +        throw({bad_request, <<"Referer header required.">>});
 +    Referer ->
 +        {_,RefererHost,_,_,_} = mochiweb_util:urlsplit(Referer),
 +        if
 +            RefererHost =:= Host -> ok;
 +            true -> throw({bad_request, <<"Referer header must match host.">>})
 +        end
 +    end.
 +
 +validate_ctype(Req, Ctype) ->
 +    case header_value(Req, "Content-Type") of
 +    undefined ->
 +        throw({bad_ctype, "Content-Type must be "++Ctype});
 +    ReqCtype ->
 +        case string:tokens(ReqCtype, ";") of
 +        [Ctype] -> ok;
 +        [Ctype, _Rest] -> ok;
 +        _Else ->
 +            throw({bad_ctype, "Content-Type must be "++Ctype})
 +        end
 +    end.
 +
 +% Utilities
 +
 +partition(Path) ->
 +    mochiweb_util:partition(Path, "/").
 +
 +header_value(#httpd{mochi_req=MochiReq}, Key) ->
 +    MochiReq:get_header_value(Key).
 +
 +header_value(#httpd{mochi_req=MochiReq}, Key, Default) ->
 +    case MochiReq:get_header_value(Key) of
 +    undefined -> Default;
 +    Value -> Value
 +    end.
 +
 +primary_header_value(#httpd{mochi_req=MochiReq}, Key) ->
 +    MochiReq:get_primary_header_value(Key).
 +
 +accepted_encodings(#httpd{mochi_req=MochiReq}) ->
 +    case MochiReq:accepted_encodings(["gzip", "identity"]) of
 +    bad_accept_encoding_value ->
 +        throw(bad_accept_encoding_value);
 +    [] ->
 +        throw(unacceptable_encoding);
 +    EncList ->
 +        EncList
 +    end.
 +
 +serve_file(Req, RelativePath, DocumentRoot) ->
 +    serve_file(Req, RelativePath, DocumentRoot, []).
 +
 +serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot,
 +           ExtraHeaders) ->
 +    log_request(Req, 200),
 +    ResponseHeaders = server_header()
 +        ++ couch_httpd_auth:cookie_auth_header(Req, [])
 +        ++ ExtraHeaders,
 +    {ok, MochiReq:serve_file(RelativePath, DocumentRoot,
 +            couch_httpd_cors:cors_headers(Req, ResponseHeaders))}.
 +
 +qs_value(Req, Key) ->
 +    qs_value(Req, Key, undefined).
 +
 +qs_value(Req, Key, Default) ->
 +    couch_util:get_value(Key, qs(Req), Default).
 +
 +qs_json_value(Req, Key, Default) ->
 +    case qs_value(Req, Key, Default) of
 +    Default ->
 +        Default;
 +    Result ->
 +        ?JSON_DECODE(Result)
 +    end.
 +
 +qs(#httpd{mochi_req=MochiReq}) ->
 +    MochiReq:parse_qs().
 +
 +path(#httpd{mochi_req=MochiReq}) ->
 +    MochiReq:get(path).
 +
 +host_for_request(#httpd{mochi_req=MochiReq}) ->
 +    XHost = config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"),
 +    case MochiReq:get_header_value(XHost) of
 +        undefined ->
 +            case MochiReq:get_header_value("Host") of
 +                undefined ->
 +                    {ok, {Address, Port}} = case MochiReq:get(socket) of
 +                        {ssl, SslSocket} -> ssl:sockname(SslSocket);
 +                        Socket -> inet:sockname(Socket)
 +                    end,
 +                    inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port);
 +                Value1 ->
 +                    Value1
 +            end;
 +        Value -> Value
 +    end.
 +
 +absolute_uri(#httpd{mochi_req=MochiReq}=Req, Path) ->
 +    Host = host_for_request(Req),
 +    XSsl = config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"),
 +    Scheme = case MochiReq:get_header_value(XSsl) of
 +                 "on" -> "https";
 +                 _ ->
 +                     XProto = config:get("httpd", "x_forwarded_proto", "X-Forwarded-Proto"),
 +                     case MochiReq:get_header_value(XProto) of
 +                         %% Restrict to "https" and "http" schemes only
 +                         "https" -> "https";
 +                         _ -> case MochiReq:get(scheme) of
 +                                  https -> "https";
 +                                  http -> "http"
 +                              end
 +                     end
 +             end,
 +    Scheme ++ "://" ++ Host ++ Path.
 +
 +unquote(UrlEncodedString) ->
 +    mochiweb_util:unquote(UrlEncodedString).
 +
 +quote(UrlDecodedString) ->
 +    mochiweb_util:quote_plus(UrlDecodedString).
 +
 +parse_form(#httpd{mochi_req=MochiReq}) ->
 +    mochiweb_multipart:parse_form(MochiReq).
 +
 +recv(#httpd{mochi_req=MochiReq}, Len) ->
 +    MochiReq:recv(Len).
 +
 +recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) ->
 +    % Fun is called once with each chunk
 +    % Fun({Length, Binary}, State)
 +    % called with Length == 0 on the last time.
 +    MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState).
 +
 +body_length(#httpd{mochi_req=MochiReq}) ->
 +    MochiReq:get(body_length).
 +
 +body(#httpd{mochi_req=MochiReq, req_body=undefined}) ->
 +    MaxSize = list_to_integer(
 +        config:get("couchdb", "max_document_size", "4294967296")),
 +    MochiReq:recv_body(MaxSize);
 +body(#httpd{req_body=ReqBody}) ->
 +    ReqBody.
 +
 +json_body(Httpd) ->
-     ?JSON_DECODE(body(Httpd)).
++    ?JSON_DECODE(maybe_decompress(Httpd, body(Httpd))).
 +
 +json_body_obj(Httpd) ->
 +    case json_body(Httpd) of
 +        {Props} -> {Props};
 +        _Else ->
 +            throw({bad_request, "Request body must be a JSON object"})
 +    end.
 +
 +
++maybe_decompress(Httpd, Body) ->
++    case header_value(Httpd, "Content-Encoding", "identity") of
++    "gzip" ->
++        zlib:gunzip(Body);
++    "identity" ->
++        Body;
++    Else ->
++        throw({bad_ctype, [Else, " is not a supported content encoding."]})
++    end.
 +
 +doc_etag(#doc{revs={Start, [DiskRev|_]}}) ->
 +    "\"" ++ ?b2l(couch_doc:rev_to_str({Start, DiskRev})) ++ "\"".
 +
 +make_etag(Term) ->
 +    <<SigInt:128/integer>> = couch_util:md5(term_to_binary(Term)),
 +    iolist_to_binary([$", io_lib:format("~.36B", [SigInt]), $"]).
 +
 +etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) ->
 +    etag_match(Req, binary_to_list(CurrentEtag));
 +
 +etag_match(Req, CurrentEtag) ->
 +    EtagsToMatch = string:tokens(
 +        header_value(Req, "If-None-Match", ""), ", "),
 +    lists:member(CurrentEtag, EtagsToMatch).
 +
 +etag_respond(Req, CurrentEtag, RespFun) ->
 +    case etag_match(Req, CurrentEtag) of
 +    true ->
 +        % the client has this in their cache.
 +        send_response(Req, 304, [{"ETag", CurrentEtag}], <<>>);
 +    false ->
 +        % Run the function.
 +        RespFun()
 +    end.
 +
 +etag_maybe(Req, RespFun) ->
 +    try
 +        RespFun()
 +    catch
 +        throw:{etag_match, ETag} ->
 +            send_response(Req, 304, [{"ETag", ETag}], <<>>)
 +    end.
 +
 +verify_is_server_admin(#httpd{user_ctx=UserCtx}) ->
 +    verify_is_server_admin(UserCtx);
 +verify_is_server_admin(#user_ctx{roles=Roles}) ->
 +    case lists:member(<<"_admin">>, Roles) of
 +    true -> ok;
 +    false -> throw({unauthorized, <<"You are not a server admin.">>})
 +    end.
 +
- log_request(#httpd{mochi_req=MochiReq,peer=Peer}, Code) ->
-     case erlang:get(dont_log_request) of
-         true ->
-             ok;
-         _ ->
-             couch_log:notice("~s - - ~s ~s ~B", [
-                 Peer,
-                 MochiReq:get(method),
-                 MochiReq:get(raw_path),
-                 Code
-             ])
-     end.
++log_request(#httpd{mochi_req=MochiReq,peer=Peer}=Req, Code) ->
++    couch_log:notice("~s - - ~s ~s ~B", [
++        Peer,
++        MochiReq:get(method),
++        MochiReq:get(raw_path),
++        Code
++    ]),
++    gen_event:notify(couch_plugin, {log_request, Req, Code}).
 +
 +start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) ->
 +    log_request(Req, Code),
 +    couch_stats_collector:increment({httpd_status_codes, Code}),
 +    Headers1 = Headers ++ server_header() ++
 +               couch_httpd_auth:cookie_auth_header(Req, Headers),
 +    Headers2 = couch_httpd_cors:cors_headers(Req, Headers1),
 +    Resp = MochiReq:start_response_length({Code, Headers2, Length}),
 +    case MochiReq:get(method) of
 +    'HEAD' -> throw({http_head_abort, Resp});
 +    _ -> ok
 +    end,
 +    {ok, Resp}.
 +
 +start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
 +    log_request(Req, Code),
 +    couch_stats_collector:increment({httpd_status_codes, Code}),
 +    CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers),
 +    Headers1 = Headers ++ server_header() ++ CookieHeader,
 +    Headers2 = couch_httpd_cors:cors_headers(Req, Headers1),
 +    Resp = MochiReq:start_response({Code, Headers2}),
 +    case MochiReq:get(method) of
 +        'HEAD' -> throw({http_head_abort, Resp});
 +        _ -> ok
 +    end,
 +    {ok, Resp}.
 +
 +send(Resp, Data) ->
 +    Resp:send(Data),
 +    {ok, Resp}.
 +
 +no_resp_conn_header([]) ->
 +    true;
 +no_resp_conn_header([{Hdr, _}|Rest]) ->
 +    case string:to_lower(Hdr) of
 +        "connection" -> false;
 +        _ -> no_resp_conn_header(Rest)
 +    end.
 +
 +http_1_0_keep_alive(Req, Headers) ->
 +    KeepOpen = Req:should_close() == false,
 +    IsHttp10 = Req:get(version) == {1, 0},
 +    NoRespHeader = no_resp_conn_header(Headers),
 +    case KeepOpen andalso IsHttp10 andalso NoRespHeader of
 +        true -> [{"Connection", "Keep-Alive"} | Headers];
 +        false -> Headers
 +    end.
 +
 +start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
 +    log_request(Req, Code),
 +    couch_stats_collector:increment({httpd_status_codes, Code}),
 +    Headers1 = http_1_0_keep_alive(MochiReq, Headers),
 +    Headers2 = Headers1 ++ server_header() ++
 +               couch_httpd_auth:cookie_auth_header(Req, Headers1),
 +    Headers3 = couch_httpd_cors:cors_headers(Req, Headers2),
 +    Resp = MochiReq:respond({Code, Headers3, chunked}),
 +    case MochiReq:get(method) of
 +    'HEAD' -> throw({http_head_abort, Resp});
 +    _ -> ok
 +    end,
 +    {ok, Resp}.
 +
 +send_chunk(Resp, Data) ->
 +    case iolist_size(Data) of
 +    0 -> ok; % do nothing
 +    _ -> Resp:write_chunk(Data)
 +    end,
 +    {ok, Resp}.
 +
 +last_chunk(Resp) ->
 +    Resp:write_chunk([]),
 +    {ok, Resp}.
 +
 +send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) ->
 +    log_request(Req, Code),
 +    couch_stats_collector:increment({httpd_status_codes, Code}),
 +    Headers1 = http_1_0_keep_alive(MochiReq, Headers),
 +    if Code >= 500 ->
 +        ?LOG_ERROR("httpd ~p error response:~n ~s", [Code, Body]);
 +    Code >= 400 ->
 +        ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]);
 +    true -> ok
 +    end,
 +    Headers2 = Headers1 ++ server_header() ++
 +               couch_httpd_auth:cookie_auth_header(Req, Headers1),
 +    Headers3 = couch_httpd_cors:cors_headers(Req, Headers2),
 +
 +    {ok, MochiReq:respond({Code, Headers3, Body})}.
 +
 +send_method_not_allowed(Req, Methods) ->
 +    send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")).
 +
 +send_json(Req, Value) ->
 +    send_json(Req, 200, Value).
 +
 +send_json(Req, Code, Value) ->
 +    send_json(Req, Code, [], Value).
 +
 +send_json(Req, Code, Headers, Value) ->
 +    initialize_jsonp(Req),
 +    DefaultHeaders = [
 +        {"Content-Type", negotiate_content_type(Req)},
 +        {"Cache-Control", "must-revalidate"}
 +    ],
 +    Body = [start_jsonp(), ?JSON_ENCODE(Value), end_jsonp(), $\n],
 +    send_response(Req, Code, DefaultHeaders ++ Headers, Body).
 +
 +start_json_response(Req, Code) ->
 +    start_json_response(Req, Code, []).
 +
 +start_json_response(Req, Code, Headers) ->
 +    initialize_jsonp(Req),
 +    DefaultHeaders = [
 +        {"Content-Type", negotiate_content_type(Req)},
 +        {"Cache-Control", "must-revalidate"}
 +    ],
 +    {ok, Resp} = start_chunked_response(Req, Code, DefaultHeaders ++ Headers),
 +    case start_jsonp() of
 +        [] -> ok;
 +        Start -> send_chunk(Resp, Start)
 +    end,
 +    {ok, Resp}.
 +
 +end_json_response(Resp) ->
 +    send_chunk(Resp, end_jsonp() ++ [$\n]),
 +    last_chunk(Resp).
 +
 +initialize_jsonp(Req) ->
 +    case get(jsonp) of
 +        undefined -> put(jsonp, qs_value(Req, "callback", no_jsonp));
 +        _ -> ok
 +    end,
 +    case get(jsonp) of
 +        no_jsonp -> [];
 +        [] -> [];
 +        CallBack ->
 +            try
 +                % make sure jsonp is configured on (default off)
 +                case config:get("httpd", "allow_jsonp", "false") of
 +                "true" ->
 +                    validate_callback(CallBack);
 +                _Else ->
 +                    put(jsonp, no_jsonp)
 +                end
 +            catch
 +                Error ->
 +                    put(jsonp, no_jsonp),
 +                    throw(Error)
 +            end
 +    end.
 +
 +start_jsonp() ->
 +    case get(jsonp) of
 +        no_jsonp -> [];
 +        [] -> [];
 +        CallBack -> ["/* CouchDB */", CallBack, "("]
 +    end.
 +
 +end_jsonp() ->
 +    case erlang:erase(jsonp) of
 +        no_jsonp -> [];
 +        [] -> [];
 +        _ -> ");"
 +    end.
 +
 +validate_callback(CallBack) when is_binary(CallBack) ->
 +    validate_callback(binary_to_list(CallBack));
 +validate_callback([]) ->
 +    ok;
 +validate_callback([Char | Rest]) ->
 +    case Char of
 +        _ when Char >= $a andalso Char =< $z -> ok;
 +        _ when Char >= $A andalso Char =< $Z -> ok;
 +        _ when Char >= $0 andalso Char =< $9 -> ok;
 +        _ when Char == $. -> ok;
 +        _ when Char == $_ -> ok;
 +        _ when Char == $[ -> ok;
 +        _ when Char == $] -> ok;
 +        _ ->
 +            throw({bad_request, invalid_callback})
 +    end,
 +    validate_callback(Rest).
 +
 +
 +error_info({Error, Reason}) when is_list(Reason) ->
 +    error_info({Error, ?l2b(Reason)});
 +error_info(bad_request) ->
 +    {400, <<"bad_request">>, <<>>};
 +error_info({bad_request, Reason}) ->
 +    {400, <<"bad_request">>, Reason};
 +error_info({query_parse_error, Reason}) ->
 +    {400, <<"query_parse_error">>, Reason};
 +% Prior art for md5 mismatch resulting in a 400 is from AWS S3
 +error_info(md5_mismatch) ->
 +    {400, <<"content_md5_mismatch">>, <<"Possible message corruption.">>};
 +error_info(not_found) ->
 +    {404, <<"not_found">>, <<"missing">>};
 +error_info({not_found, Reason}) ->
 +    {404, <<"not_found">>, Reason};
 +error_info({not_acceptable, Reason}) ->
 +    {406, <<"not_acceptable">>, Reason};
 +error_info(conflict) ->
 +    {409, <<"conflict">>, <<"Document update conflict.">>};
 +error_info({forbidden, Msg}) ->
 +    {403, <<"forbidden">>, Msg};
 +error_info({unauthorized, Msg}) ->
 +    {401, <<"unauthorized">>, Msg};
 +error_info(file_exists) ->
 +    {412, <<"file_exists">>, <<"The database could not be "
 +        "created, the file already exists.">>};
 +error_info(request_entity_too_large) ->
 +    {413, <<"too_large">>, <<"the request entity is too large">>};
 +error_info({bad_ctype, Reason}) ->
 +    {415, <<"bad_content_type">>, Reason};
 +error_info(requested_range_not_satisfiable) ->
 +    {416, <<"requested_range_not_satisfiable">>, <<"Requested range not satisfiable">>};
 +error_info({error, illegal_database_name, Name}) ->
 +    Message = "Name: '" ++ Name ++ "'. Only lowercase characters (a-z), "
 +        ++ "digits (0-9), and any of the characters _, $, (, ), +, -, and / "
 +        ++ "are allowed. Must begin with a letter.",
 +    {400, <<"illegal_database_name">>, couch_util:to_binary(Message)};
 +error_info({missing_stub, Reason}) ->
 +    {412, <<"missing_stub">>, Reason};
 +error_info({Error, Reason}) ->
 +    {500, couch_util:to_binary(Error), couch_util:to_binary(Reason)};
 +error_info(Error) ->
 +    {500, <<"unknown_error">>, couch_util:to_binary(Error)}.
 +
 +error_headers(#httpd{mochi_req=MochiReq}=Req, Code, ErrorStr, ReasonStr) ->
 +    if Code == 401 ->
 +        % this is where the basic auth popup is triggered
 +        case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of
 +        undefined ->
 +            case config:get("httpd", "WWW-Authenticate", nil) of
 +            nil ->
 +                % If the client is a browser and the basic auth popup isn't turned on
 +                % redirect to the session page.
 +                case ErrorStr of
 +                <<"unauthorized">> ->
 +                    case config:get("couch_httpd_auth", "authentication_redirect", nil) of
 +                    nil -> {Code, []};
 +                    AuthRedirect ->
 +                        case config:get("couch_httpd_auth", "require_valid_user", "false") of
 +                        "true" ->
 +                            % send the browser popup header no matter what if we are require_valid_user
 +                            {Code, [{"WWW-Authenticate", "Basic realm=\"server\""}]};
 +                        _False ->
 +                            case MochiReq:accepts_content_type("application/json") of
 +                            true ->
 +                                {Code, []};
 +                            false ->
 +                                case MochiReq:accepts_content_type("text/html") of
 +                                true ->
 +                                    % Redirect to the path the user requested, not
 +                                    % the one that is used internally.
 +                                    UrlReturnRaw = case MochiReq:get_header_value("x-couchdb-vhost-path") of
 +                                    undefined ->
 +                                        MochiReq:get(path);
 +                                    VHostPath ->
 +                                        VHostPath
 +                                    end,
 +                                    RedirectLocation = lists:flatten([
 +                                        AuthRedirect,
 +                                        "?return=", couch_util:url_encode(UrlReturnRaw),
 +                                        "&reason=", couch_util:url_encode(ReasonStr)
 +                                    ]),
 +                                    {302, [{"Location", absolute_uri(Req, RedirectLocation)}]};
 +                                false ->
 +                                    {Code, []}
 +                                end
 +                            end
 +                        end
 +                    end;
 +                _Else ->
 +                    {Code, []}
 +                end;
 +            Type ->
 +                {Code, [{"WWW-Authenticate", Type}]}
 +            end;
 +        Type ->
 +           {Code, [{"WWW-Authenticate", Type}]}
 +        end;
 +    true ->
 +        {Code, []}
 +    end.
 +
 +send_error(_Req, {already_sent, Resp, _Error}) ->
 +    {ok, Resp};
 +
 +send_error(Req, Error) ->
 +    {Code, ErrorStr, ReasonStr} = error_info(Error),
 +    {Code1, Headers} = error_headers(Req, Code, ErrorStr, ReasonStr),
 +    send_error(Req, Code1, Headers, ErrorStr, ReasonStr).
 +
 +send_error(Req, Code, ErrorStr, ReasonStr) ->
 +    send_error(Req, Code, [], ErrorStr, ReasonStr).
 +
 +send_error(Req, Code, Headers, ErrorStr, ReasonStr) ->
 +    send_json(Req, Code, Headers,
 +        {[{<<"error">>,  ErrorStr},
 +         {<<"reason">>, ReasonStr}]}).
 +
 +% give the option for list functions to output html or other raw errors
 +send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) ->
 +    send_chunk(Resp, Reason),
 +    last_chunk(Resp);
 +
 +send_chunked_error(Resp, Error) ->
 +    {Code, ErrorStr, ReasonStr} = error_info(Error),
 +    JsonError = {[{<<"code">>, Code},
 +        {<<"error">>,  ErrorStr},
 +        {<<"reason">>, ReasonStr}]},
 +    send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])),
 +    last_chunk(Resp).
 +
 +send_redirect(Req, Path) ->
 +     send_response(Req, 301, [{"Location", absolute_uri(Req, Path)}], <<>>).
 +
 +negotiate_content_type(Req) ->
 +    case get(jsonp) of
 +        no_jsonp -> negotiate_content_type1(Req);
 +        [] -> negotiate_content_type1(Req);
 +        _Callback -> "text/javascript"
 +    end.
 +
 +negotiate_content_type1(#httpd{mochi_req=MochiReq}) ->
 +    %% Determine the appropriate Content-Type header for a JSON response
 +    %% depending on the Accept header in the request. A request that explicitly
 +    %% lists the correct JSON MIME type will get that type, otherwise the
 +    %% response will have the generic MIME type "text/plain"
 +    AcceptedTypes = case MochiReq:get_header_value("Accept") of
 +        undefined       -> [];
 +        AcceptHeader    -> string:tokens(AcceptHeader, ", ")
 +    end,
 +    case lists:member("application/json", AcceptedTypes) of
 +        true  -> "application/json";
 +        false -> "text/plain; charset=utf-8"
 +    end.
 +
 +server_header() ->
 +    [{"Server", "CouchDB/" ++ couch_server:get_version() ++
 +                " (Erlang OTP/" ++ erlang:system_info(otp_release) ++ ")"}].
 +
 +
 +-record(mp, {boundary, buffer, data_fun, callback}).
 +
 +
 +parse_multipart_request(ContentType, DataFun, Callback) ->
 +    Boundary0 = iolist_to_binary(get_boundary(ContentType)),
 +    Boundary = <<"\r\n--", Boundary0/binary>>,
 +    Mp = #mp{boundary= Boundary,
 +            buffer= <<>>,
 +            data_fun=DataFun,
 +            callback=Callback},
 +    {Mp2, _NilCallback} = read_until(Mp, <<"--", Boundary0/binary>>,
 +        fun nil_callback/1),
 +    #mp{buffer=Buffer, data_fun=DataFun2, callback=Callback2} =
 +            parse_part_header(Mp2),
 +    {Buffer, DataFun2, Callback2}.
 +
 +nil_callback(_Data)->
 +    fun nil_callback/1.
 +
 +get_boundary({"multipart/" ++ _, Opts}) ->
 +    case couch_util:get_value("boundary", Opts) of
 +        S when is_list(S) ->
 +            S
 +    end;
 +get_boundary(ContentType) ->
 +    {"multipart/" ++ _ , Opts} = mochiweb_util:parse_header(ContentType),
 +    get_boundary({"multipart/", Opts}).
 +
 +
 +
 +split_header(<<>>) ->
 +    [];
 +split_header(Line) ->
 +    {Name, [$: | Value]} = lists:splitwith(fun (C) -> C =/= $: end,
 +                                           binary_to_list(Line)),
 +    [{string:to_lower(string:strip(Name)),
 +     mochiweb_util:parse_header(Value)}].
 +
 +read_until(#mp{data_fun=DataFun, buffer=Buffer}=Mp, Pattern, Callback) ->
-     case find_in_binary(Pattern, Buffer) of
++    case couch_util:find_in_binary(Pattern, Buffer) of
 +    not_found ->
 +        Callback2 = Callback(Buffer),
 +        {Buffer2, DataFun2} = DataFun(),
 +        Buffer3 = iolist_to_binary(Buffer2),
 +        read_until(Mp#mp{data_fun=DataFun2,buffer=Buffer3}, Pattern, Callback2);
 +    {partial, 0} ->
 +        {NewData, DataFun2} = DataFun(),
 +        read_until(Mp#mp{data_fun=DataFun2,
 +                buffer= iolist_to_binary([Buffer,NewData])},
 +                Pattern, Callback);
 +    {partial, Skip} ->
 +        <<DataChunk:Skip/binary, Rest/binary>> = Buffer,
 +        Callback2 = Callback(DataChunk),
 +        {NewData, DataFun2} = DataFun(),
 +        read_until(Mp#mp{data_fun=DataFun2,
 +                buffer= iolist_to_binary([Rest | NewData])},
 +                Pattern, Callback2);
 +    {exact, 0} ->
 +        PatternLen = size(Pattern),
 +        <<_:PatternLen/binary, Rest/binary>> = Buffer,
 +        {Mp#mp{buffer= Rest}, Callback};
 +    {exact, Skip} ->
 +        PatternLen = size(Pattern),
 +        <<DataChunk:Skip/binary, _:PatternLen/binary, Rest/binary>> = Buffer,
 +        Callback2 = Callback(DataChunk),
 +        {Mp#mp{buffer= Rest}, Callback2}
 +    end.
 +
 +
 +parse_part_header(#mp{callback=UserCallBack}=Mp) ->
 +    {Mp2, AccCallback} = read_until(Mp, <<"\r\n\r\n">>,
 +            fun(Next) -> acc_callback(Next, []) end),
 +    HeaderData = AccCallback(get_data),
 +
 +    Headers =
 +    lists:foldl(fun(Line, Acc) ->
 +            split_header(Line) ++ Acc
 +        end, [], re:split(HeaderData,<<"\r\n">>, [])),
 +    NextCallback = UserCallBack({headers, Headers}),
 +    parse_part_body(Mp2#mp{callback=NextCallback}).
 +
 +parse_part_body(#mp{boundary=Prefix, callback=Callback}=Mp) ->
 +    {Mp2, WrappedCallback} = read_until(Mp, Prefix,
 +            fun(Data) -> body_callback_wrapper(Data, Callback) end),
 +    Callback2 = WrappedCallback(get_callback),
 +    Callback3 = Callback2(body_end),
 +    case check_for_last(Mp2#mp{callback=Callback3}) of
 +    {last, #mp{callback=Callback3}=Mp3} ->
 +        Mp3#mp{callback=Callback3(eof)};
 +    {more, Mp3} ->
 +        parse_part_header(Mp3)
 +    end.
 +
 +acc_callback(get_data, Acc)->
 +    iolist_to_binary(lists:reverse(Acc));
 +acc_callback(Data, Acc)->
 +    fun(Next) -> acc_callback(Next, [Data | Acc]) end.
 +
 +body_callback_wrapper(get_callback, Callback) ->
 +    Callback;
 +body_callback_wrapper(Data, Callback) ->
 +    Callback2 = Callback({body, Data}),
 +    fun(Next) -> body_callback_wrapper(Next, Callback2) end.
 +
 +
 +check_for_last(#mp{buffer=Buffer, data_fun=DataFun}=Mp) ->
 +    case Buffer of
 +    <<"--",_/binary>> -> {last, Mp};
 +    <<_, _, _/binary>> -> {more, Mp};
 +    _ -> % not long enough
 +        {Data, DataFun2} = DataFun(),
 +        check_for_last(Mp#mp{buffer= <<Buffer/binary, Data/binary>>,
 +                data_fun = DataFun2})
 +    end.
 +
- find_in_binary(_B, <<>>) ->
-     not_found;
- 
- find_in_binary(B, Data) ->
-     case binary:match(Data, [B], []) of
-     nomatch ->
-         partial_find(binary:part(B, {0, byte_size(B) - 1}),
-                      binary:part(Data, {byte_size(Data), -byte_size(Data) + 1}), 1);
-     {Pos, _Len} ->
-         {exact, Pos}
-     end.
- 
- partial_find(<<>>, _Data, _Pos) ->
-     not_found;
- 
- partial_find(B, Data, N) when byte_size(Data) > 0 ->
-     case binary:match(Data, [B], []) of
-     nomatch ->
-         partial_find(binary:part(B, {0, byte_size(B) - 1}),
-                      binary:part(Data, {byte_size(Data), -byte_size(Data) + 1}), N + 1);
-     {Pos, _Len} ->
-         {partial, N + Pos}
-     end;
- 
- partial_find(_B, _Data, _N) ->
-     not_found.
- 
- 
 +validate_bind_address(Address) ->
 +    case inet_parse:address(Address) of
 +        {ok, _} -> ok;
 +        _ -> throw({error, invalid_bind_address})
 +    end.

http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/9d629ff6/src/couch_httpd_auth.erl
----------------------------------------------------------------------
diff --cc src/couch_httpd_auth.erl
index a747869,0000000..c982167
mode 100644,000000..100644
--- a/src/couch_httpd_auth.erl
+++ b/src/couch_httpd_auth.erl
@@@ -1,376 -1,0 +1,380 @@@
 +% 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_auth).
 +-include_lib("couch/include/couch_db.hrl").
 +
 +-export([default_authentication_handler/1,special_test_authentication_handler/1]).
 +-export([cookie_authentication_handler/1]).
 +-export([null_authentication_handler/1]).
- -export([proxy_authentification_handler/1]).
++-export([proxy_authentication_handler/1, proxy_authentification_handler/1]).
 +-export([cookie_auth_header/2]).
 +-export([handle_session_req/1]).
 +
 +-import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]).
 +
 +special_test_authentication_handler(Req) ->
 +    case header_value(Req, "WWW-Authenticate") of
 +    "X-Couch-Test-Auth " ++ NamePass ->
 +        % NamePass is a colon separated string: "joe schmoe:a password".
 +        [Name, Pass] = re:split(NamePass, ":", [{return, list}, {parts, 2}]),
 +        case {Name, Pass} of
 +        {"Jan Lehnardt", "apple"} -> ok;
 +        {"Christopher Lenz", "dog food"} -> ok;
 +        {"Noah Slater", "biggiesmalls endian"} -> ok;
 +        {"Chris Anderson", "mp3"} -> ok;
 +        {"Damien Katz", "pecan pie"} -> ok;
 +        {_, _} ->
 +            throw({unauthorized, <<"Name or password is incorrect.">>})
 +        end,
 +        Req#httpd{user_ctx=#user_ctx{name=?l2b(Name)}};
 +    _ ->
 +        % No X-Couch-Test-Auth credentials sent, give admin access so the
 +        % previous authentication can be restored after the test
 +        Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}
 +    end.
 +
 +basic_name_pw(Req) ->
 +    AuthorizationHeader = header_value(Req, "Authorization"),
 +    case AuthorizationHeader of
 +    "Basic " ++ Base64Value ->
 +        case re:split(base64:decode(Base64Value), ":",
 +                      [{return, list}, {parts, 2}]) of
 +        ["_", "_"] ->
 +            % special name and pass to be logged out
 +            nil;
 +        [User, Pass] ->
 +            {User, Pass};
 +        _ ->
 +            nil
 +        end;
 +    _ ->
 +        nil
 +    end.
 +
 +default_authentication_handler(Req) ->
 +    case basic_name_pw(Req) of
 +    {User, Pass} ->
 +        case couch_auth_cache:get_user_creds(User) of
 +            nil ->
 +                throw({unauthorized, <<"Name or password is incorrect.">>});
 +            UserProps ->
 +                case authenticate(?l2b(Pass), UserProps) of
 +                    true ->
 +                        Req#httpd{user_ctx=#user_ctx{
 +                            name=?l2b(User),
 +                            roles=couch_util:get_value(<<"roles">>, UserProps, [])
 +                        }};
 +                    _Else ->
 +                        throw({unauthorized, <<"Name or password is incorrect.">>})
 +                end
 +        end;
 +    nil ->
 +        case couch_server:has_admins() of
 +        true ->
 +            Req;
 +        false ->
 +            case config:get("couch_httpd_auth", "require_valid_user", "false") of
 +                "true" -> Req;
 +                % If no admins, and no user required, then everyone is admin!
 +                % Yay, admin party!
 +                _ -> Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}
 +            end
 +        end
 +    end.
 +
 +null_authentication_handler(Req) ->
 +    Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}.
 +
 +%% @doc proxy auth handler.
 +%
 +% This handler allows creation of a userCtx object from a user authenticated remotly.
 +% The client just pass specific headers to CouchDB and the handler create the userCtx.
 +% Headers  name can be defined in local.ini. By thefault they are :
 +%
 +%   * X-Auth-CouchDB-UserName : contain the username, (x_auth_username in
 +%   couch_httpd_auth section)
 +%   * X-Auth-CouchDB-Roles : contain the user roles, list of roles separated by a
 +%   comma (x_auth_roles in couch_httpd_auth section)
 +%   * X-Auth-CouchDB-Token : token to authenticate the authorization (x_auth_token
 +%   in couch_httpd_auth section). This token is an hmac-sha1 created from secret key
 +%   and username. The secret key should be the same in the client and couchdb node. s
 +%   ecret key is the secret key in couch_httpd_auth section of ini. This token is optional
 +%   if value of proxy_use_secret key in couch_httpd_auth section of ini isn't true.
 +%
- proxy_authentification_handler(Req) ->
++proxy_authentication_handler(Req) ->
 +    case proxy_auth_user(Req) of
 +        nil -> Req;
 +        Req2 -> Req2
 +    end.
++
++%% @deprecated
++proxy_authentification_handler(Req) ->
++    proxy_authentication_handler(Req).
 +    
 +proxy_auth_user(Req) ->
 +    XHeaderUserName = config:get("couch_httpd_auth", "x_auth_username",
 +                                "X-Auth-CouchDB-UserName"),
 +    XHeaderRoles = config:get("couch_httpd_auth", "x_auth_roles",
 +                                "X-Auth-CouchDB-Roles"),
 +    XHeaderToken = config:get("couch_httpd_auth", "x_auth_token",
 +                                "X-Auth-CouchDB-Token"),
 +    case header_value(Req, XHeaderUserName) of
 +        undefined -> nil;
 +        UserName ->
 +            Roles = case header_value(Req, XHeaderRoles) of
 +                undefined -> [];
 +                Else ->
 +                    [?l2b(R) || R <- string:tokens(Else, ",")]
 +            end,
 +            case config:get("couch_httpd_auth", "proxy_use_secret", "false") of
 +                "true" ->
 +                    case config:get("couch_httpd_auth", "secret", nil) of
 +                        nil ->
 +                            Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName), roles=Roles}};
 +                        Secret ->
 +                            ExpectedToken = couch_util:to_hex(crypto:sha_mac(Secret, UserName)),
 +                            case header_value(Req, XHeaderToken) of
 +                                Token when Token == ExpectedToken ->
 +                                    Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName),
 +                                                            roles=Roles}};
 +                                _ -> nil
 +                            end
 +                    end;
 +                _ ->
 +                    Req#httpd{user_ctx=#user_ctx{name=?l2b(UserName), roles=Roles}}
 +            end
 +    end.
 +
 +
 +cookie_authentication_handler(#httpd{mochi_req=MochiReq}=Req) ->
 +    case MochiReq:get_cookie_value("AuthSession") of
 +    undefined -> Req;
 +    [] -> Req;
 +    Cookie ->
 +        [User, TimeStr, HashStr] = try
 +            AuthSession = couch_util:decodeBase64Url(Cookie),
 +            [_A, _B, _Cs] = re:split(?b2l(AuthSession), ":",
 +                                     [{return, list}, {parts, 3}])
 +        catch
 +            _:_Error ->
 +                Reason = <<"Malformed AuthSession cookie. Please clear your cookies.">>,
 +                throw({bad_request, Reason})
 +        end,
 +        % Verify expiry and hash
 +        CurrentTime = make_cookie_time(),
 +        case config:get("couch_httpd_auth", "secret", nil) of
 +        nil ->
 +            ?LOG_DEBUG("cookie auth secret is not set",[]),
 +            Req;
 +        SecretStr ->
 +            Secret = ?l2b(SecretStr),
 +            case couch_auth_cache:get_user_creds(User) of
 +            nil -> Req;
 +            UserProps ->
 +                UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<"">>),
 +                FullSecret = <<Secret/binary, UserSalt/binary>>,
 +                ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr),
 +                Hash = ?l2b(HashStr),
 +                Timeout = list_to_integer(
 +                    config:get("couch_httpd_auth", "timeout", "600")),
 +                ?LOG_DEBUG("timeout ~p", [Timeout]),
 +                case (catch erlang:list_to_integer(TimeStr, 16)) of
 +                    TimeStamp when CurrentTime < TimeStamp + Timeout ->
 +                        case couch_passwords:verify(ExpectedHash, Hash) of
 +                            true ->
 +                                TimeLeft = TimeStamp + Timeout - CurrentTime,
 +                                ?LOG_DEBUG("Successful cookie auth as: ~p", [User]),
 +                                Req#httpd{user_ctx=#user_ctx{
 +                                    name=?l2b(User),
 +                                    roles=couch_util:get_value(<<"roles">>, UserProps, [])
 +                                }, auth={FullSecret, TimeLeft < Timeout*0.9}};
 +                            _Else ->
 +                                Req
 +                        end;
 +                    _Else ->
 +                        Req
 +                end
 +            end
 +        end
 +    end.
 +
 +cookie_auth_header(#httpd{user_ctx=#user_ctx{name=null}}, _Headers) -> [];
 +cookie_auth_header(#httpd{user_ctx=#user_ctx{name=User}, auth={Secret, true}}=Req, Headers) ->
 +    % Note: we only set the AuthSession cookie if:
 +    %  * a valid AuthSession cookie has been received
 +    %  * we are outside a 10% timeout window
 +    %  * and if an AuthSession cookie hasn't already been set e.g. by a login
 +    %    or logout handler.
 +    % The login and logout handlers need to set the AuthSession cookie
 +    % themselves.
 +    CookieHeader = couch_util:get_value("Set-Cookie", Headers, ""),
 +    Cookies = mochiweb_cookies:parse_cookie(CookieHeader),
 +    AuthSession = couch_util:get_value("AuthSession", Cookies),
 +    if AuthSession == undefined ->
 +        TimeStamp = make_cookie_time(),
 +        [cookie_auth_cookie(Req, ?b2l(User), Secret, TimeStamp)];
 +    true ->
 +        []
 +    end;
 +cookie_auth_header(_Req, _Headers) -> [].
 +
 +cookie_auth_cookie(Req, User, Secret, TimeStamp) ->
 +    SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16),
 +    Hash = crypto:sha_mac(Secret, SessionData),
 +    mochiweb_cookies:cookie("AuthSession",
 +        couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)),
 +        [{path, "/"}] ++ cookie_scheme(Req) ++ max_age()).
 +
 +ensure_cookie_auth_secret() ->
 +    case config:get("couch_httpd_auth", "secret", nil) of
 +        nil ->
 +            NewSecret = ?b2l(couch_uuids:random()),
 +            config:set("couch_httpd_auth", "secret", NewSecret),
 +            NewSecret;
 +        Secret -> Secret
 +    end.
 +
 +% session handlers
 +% Login handler with user db
 +handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
 +    ReqBody = MochiReq:recv_body(),
 +    Form = case MochiReq:get_primary_header_value("content-type") of
 +        % content type should be json
 +        "application/x-www-form-urlencoded" ++ _ ->
 +            mochiweb_util:parse_qs(ReqBody);
 +        "application/json" ++ _ ->
 +            {Pairs} = ?JSON_DECODE(ReqBody),
 +            lists:map(fun({Key, Value}) ->
 +              {?b2l(Key), ?b2l(Value)}
 +            end, Pairs);
 +        _ ->
 +            []
 +    end,
 +    UserName = ?l2b(couch_util:get_value("name", Form, "")),
 +    Password = ?l2b(couch_util:get_value("password", Form, "")),
 +    ?LOG_DEBUG("Attempt Login: ~s",[UserName]),
 +    User = case couch_auth_cache:get_user_creds(UserName) of
 +        nil -> [];
 +        Result -> Result
 +    end,
 +    UserSalt = couch_util:get_value(<<"salt">>, User, <<>>),
 +    case authenticate(Password, User) of
 +        true ->
 +            % setup the session cookie
 +            Secret = ?l2b(ensure_cookie_auth_secret()),
 +            CurrentTime = make_cookie_time(),
 +            Cookie = cookie_auth_cookie(Req, ?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
 +            % TODO document the "next" feature in Futon
 +            {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
 +                nil ->
 +                    {200, [Cookie]};
 +                Redirect ->
 +                    {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
 +            end,
 +            send_json(Req#httpd{req_body=ReqBody}, Code, Headers,
 +                {[
 +                    {ok, true},
 +                    {name, couch_util:get_value(<<"name">>, User, null)},
 +                    {roles, couch_util:get_value(<<"roles">>, User, [])}
 +                ]});
 +        _Else ->
 +            % clear the session
 +            Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}] ++ cookie_scheme(Req)),
 +            {Code, Headers} = case couch_httpd:qs_value(Req, "fail", nil) of
 +                nil ->
 +                    {401, [Cookie]};
 +                Redirect ->
 +                    {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
 +            end,
 +            send_json(Req, Code, Headers, {[{error, <<"unauthorized">>},{reason, <<"Name or password is incorrect.">>}]})
 +    end;
 +% get user info
 +% GET /_session
 +handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
 +    Name = UserCtx#user_ctx.name,
 +    ForceLogin = couch_httpd:qs_value(Req, "basic", "false"),
 +    case {Name, ForceLogin} of
 +        {null, "true"} ->
 +            throw({unauthorized, <<"Please login.">>});
 +        {Name, _} ->
 +            send_json(Req, {[
 +                % remove this ok
 +                {ok, true},
 +                {<<"userCtx">>, {[
 +                    {name, Name},
 +                    {roles, UserCtx#user_ctx.roles}
 +                ]}},
 +                {info, {[
 +                    {authentication_db, ?l2b(config:get("couch_httpd_auth", "authentication_db"))},
 +                    {authentication_handlers, [auth_name(H) || H <- couch_httpd:make_fun_spec_strs(
 +                            config:get("httpd", "authentication_handlers"))]}
 +                ] ++ maybe_value(authenticated, UserCtx#user_ctx.handler, fun(Handler) ->
 +                        auth_name(?b2l(Handler))
 +                    end)}}
 +            ]})
 +    end;
 +% logout by deleting the session
 +handle_session_req(#httpd{method='DELETE'}=Req) ->
 +    Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}] ++ cookie_scheme(Req)),
 +    {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
 +        nil ->
 +            {200, [Cookie]};
 +        Redirect ->
 +            {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
 +    end,
 +    send_json(Req, Code, Headers, {[{ok, true}]});
 +handle_session_req(Req) ->
 +    send_method_not_allowed(Req, "GET,HEAD,POST,DELETE").
 +
 +maybe_value(_Key, undefined, _Fun) -> [];
 +maybe_value(Key, Else, Fun) ->
 +    [{Key, Fun(Else)}].
 +
 +authenticate(Pass, UserProps) ->
 +    UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<>>),
 +    {PasswordHash, ExpectedHash} =
 +        case couch_util:get_value(<<"password_scheme">>, UserProps, <<"simple">>) of
 +        <<"simple">> ->
 +            {couch_passwords:simple(Pass, UserSalt),
 +            couch_util:get_value(<<"password_sha">>, UserProps, nil)};
 +        <<"pbkdf2">> ->
 +            Iterations = couch_util:get_value(<<"iterations">>, UserProps, 10000),
 +            {couch_passwords:pbkdf2(Pass, UserSalt, Iterations),
 +             couch_util:get_value(<<"derived_key">>, UserProps, nil)}
 +    end,
 +    couch_passwords:verify(PasswordHash, ExpectedHash).
 +
 +auth_name(String) when is_list(String) ->
 +    [_,_,_,_,_,Name|_] = re:split(String, "[\\W_]", [{return, list}]),
 +    ?l2b(Name).
 +
 +make_cookie_time() ->
 +    {NowMS, NowS, _} = erlang:now(),
 +    NowMS * 1000000 + NowS.
 +
 +cookie_scheme(#httpd{mochi_req=MochiReq}) ->
 +    [{http_only, true}] ++
 +    case MochiReq:get(scheme) of
 +        http -> [];
 +        https -> [{secure, true}]
 +    end.
 +
 +max_age() ->
 +    case config:get("couch_httpd_auth", "allow_persistent_cookies", "false") of
 +        "false" ->
 +            [];
 +        "true" ->
 +            Timeout = list_to_integer(
 +                config:get("couch_httpd_auth", "timeout", "600")),
 +            [{max_age, Timeout}]
 +    end.

http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/9d629ff6/src/couch_httpd_cors.erl
----------------------------------------------------------------------
diff --cc src/couch_httpd_cors.erl
index d98357a,0000000..15b838e
mode 100644,000000..100644
--- a/src/couch_httpd_cors.erl
+++ b/src/couch_httpd_cors.erl
@@@ -1,343 -1,0 +1,351 @@@
 +% 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.
 +
 +%% @doc module to handle Cross-Origin Resource Sharing
 +%%
- %% This module handles CROSS requests and preflight request for a
- %% couchdb Node. The config is done in the ini file.
++%% This module handles CORS requests and preflight request for
++%% CouchDB. The configuration is done in the ini file.
++%%
++%% This implements http://www.w3.org/TR/cors/
 +
 +
 +-module(couch_httpd_cors).
 +
 +-include_lib("couch/include/couch_db.hrl").
 +
 +-export([is_preflight_request/1, cors_headers/2]).
 +
 +-define(SUPPORTED_HEADERS, "Accept, Accept-Language, Content-Type," ++
 +        "Expires, Last-Modified, Pragma, Origin, Content-Length," ++
 +        "If-Match, Destination, X-Requested-With, " ++
 +        "X-Http-Method-Override, Content-Range").
 +
 +-define(SUPPORTED_METHODS, "GET, HEAD, POST, PUT, DELETE," ++
 +        "TRACE, CONNECT, COPY, OPTIONS").
 +
 +% as defined in http://www.w3.org/TR/cors/#terminology
 +-define(SIMPLE_HEADERS, ["Cache-Control", "Content-Language",
 +        "Content-Type", "Expires", "Last-Modified", "Pragma"]).
++-define(ALLOWED_HEADERS, lists:sort(["Server", "Etag",
++        "Accept-Ranges" | ?SIMPLE_HEADERS])).
 +-define(SIMPLE_CONTENT_TYPE_VALUES, ["application/x-www-form-urlencoded",
 +        "multipart/form-data", "text/plain"]).
 +
 +% TODO: - pick a sane default
 +-define(CORS_DEFAULT_MAX_AGE, 12345).
 +
 +%% is_preflight_request/1
 +
 +% http://www.w3.org/TR/cors/#resource-preflight-requests
 +
 +is_preflight_request(#httpd{method=Method}=Req) when Method /= 'OPTIONS' ->
 +    Req;
 +is_preflight_request(Req) ->
 +    EnableCors = enable_cors(),
 +    is_preflight_request(Req, EnableCors).
 +
 +is_preflight_request(Req, false) ->
 +    Req;
 +is_preflight_request(#httpd{mochi_req=MochiReq}=Req, true) ->
 +    case preflight_request(MochiReq) of
 +    {ok, PreflightHeaders} ->
 +        send_preflight_response(Req, PreflightHeaders);
 +    _ ->
 +        Req
 +    end.
 +
 +
 +preflight_request(MochiReq) ->
 +    Origin = MochiReq:get_header_value("Origin"),
 +    preflight_request(MochiReq, Origin).
 +
 +preflight_request(MochiReq, undefined) ->
 +    % If the Origin header is not present terminate this set of
 +    % steps. The request is outside the scope of this specification.
 +    % http://www.w3.org/TR/cors/#resource-preflight-requests
 +    MochiReq;
 +preflight_request(MochiReq, Origin) ->
 +    Host = couch_httpd_vhost:host(MochiReq),
 +    AcceptedOrigins = get_accepted_origins(Host),
 +    AcceptAll = lists:member("*", AcceptedOrigins),
 +
 +    HandlerFun = fun() ->
 +        OriginList = couch_util:to_list(Origin),
 +        handle_preflight_request(OriginList, Host, MochiReq)
 +    end,
 +
 +    case AcceptAll of
 +    true ->
 +        % Always matching is acceptable since the list of
 +        % origins can be unbounded.
 +        % http://www.w3.org/TR/cors/#resource-preflight-requests
 +        HandlerFun();
 +    false ->
 +        case lists:member(Origin, AcceptedOrigins) of
 +        % The Origin header can only contain a single origin as
 +        % the user agent will not follow redirects.
 +        % http://www.w3.org/TR/cors/#resource-preflight-requests
 +        % TODO: Square against multi origin thinger in Security Considerations
 +        true ->
 +            HandlerFun();
 +        false ->
 +            % If the value of the Origin header is not a
 +            % case-sensitive match for any of the values
 +            % in list of origins do not set any additional
 +            % headers and terminate this set of steps.
 +            % http://www.w3.org/TR/cors/#resource-preflight-requests
 +            false
 +        end
 +    end.
 +
 +
 +handle_preflight_request(Origin, Host, MochiReq) ->
 +    %% get supported methods
 +    SupportedMethods = split_list(cors_config(Host, "methods",
 +                                              ?SUPPORTED_METHODS)),
 +
 +    % get supported headers
 +    AllSupportedHeaders = split_list(cors_config(Host, "headers",
 +                                                 ?SUPPORTED_HEADERS)),
 +
 +    SupportedHeaders = [string:to_lower(H) || H <- AllSupportedHeaders],
 +
 +    % get max age
 +    MaxAge = cors_config(Host, "max_age", ?CORS_DEFAULT_MAX_AGE),
 +
 +    PreflightHeaders0 = maybe_add_credentials(Origin, Host, [
 +        {"Access-Control-Allow-Origin", Origin},
 +        {"Access-Control-Max-Age", MaxAge},
 +        {"Access-Control-Allow-Methods",
 +            string:join(SupportedMethods, ", ")}]),
 +
 +    case MochiReq:get_header_value("Access-Control-Request-Method") of
 +    undefined ->
 +        % If there is no Access-Control-Request-Method header
 +        % or if parsing failed, do not set any additional headers
 +        % and terminate this set of steps. The request is outside
 +        % the scope of this specification.
 +        % http://www.w3.org/TR/cors/#resource-preflight-requests
 +        {ok, PreflightHeaders0};
 +    Method ->
 +        case lists:member(Method, SupportedMethods) of
 +        true ->
 +            % method ok , check headers
 +            AccessHeaders = MochiReq:get_header_value(
 +                    "Access-Control-Request-Headers"),
 +            {FinalReqHeaders, ReqHeaders} = case AccessHeaders of
 +                undefined -> {"", []};
 +                Headers ->
 +                    % transform header list in something we
 +                    % could check. make sure everything is a
 +                    % list
 +                    RH = [string:to_lower(H)
 +                          || H <- split_headers(Headers)],
 +                    {Headers, RH}
 +            end,
 +            % check if headers are supported
 +            case ReqHeaders -- SupportedHeaders of
 +            [] ->
 +                PreflightHeaders = PreflightHeaders0 ++
 +                                   [{"Access-Control-Allow-Headers",
 +                                     FinalReqHeaders}],
 +                {ok, PreflightHeaders};
 +            _ ->
 +                false
 +            end;
 +        false ->
 +        % If method is not a case-sensitive match for any of
 +        % the values in list of methods do not set any additional
 +        % headers and terminate this set of steps.
 +        % http://www.w3.org/TR/cors/#resource-preflight-requests
 +            false
 +        end
 +    end.
 +
 +
 +send_preflight_response(#httpd{mochi_req=MochiReq}=Req, Headers) ->
 +    couch_httpd:log_request(Req, 204),
 +    couch_stats_collector:increment({httpd_status_codes, 204}),
 +    Headers1 = couch_httpd:http_1_0_keep_alive(MochiReq, Headers),
 +    Headers2 = Headers1 ++ couch_httpd:server_header() ++
 +               couch_httpd_auth:cookie_auth_header(Req, Headers1),
 +    {ok, MochiReq:respond({204, Headers2, <<>>})}.
 +
 +
 +% cors_headers/1
 +
 +cors_headers(MochiReq, RequestHeaders) ->
 +    EnableCors = enable_cors(),
 +    CorsHeaders = do_cors_headers(MochiReq, EnableCors),
 +    maybe_apply_cors_headers(CorsHeaders, RequestHeaders).
 +
 +do_cors_headers(#httpd{mochi_req=MochiReq}, true) ->
 +    Host = couch_httpd_vhost:host(MochiReq),
 +    AcceptedOrigins = get_accepted_origins(Host),
 +    case MochiReq:get_header_value("Origin") of
 +    undefined ->
 +        % If the Origin header is not present terminate
 +        % this set of steps. The request is outside the scope
 +        % of this specification.
 +        % http://www.w3.org/TR/cors/#resource-processing-model
 +        [];
 +    Origin ->
 +        handle_cors_headers(couch_util:to_list(Origin),
 +                            Host, AcceptedOrigins)
 +    end;
 +do_cors_headers(_MochiReq, false) ->
 +    [].
 +
 +maybe_apply_cors_headers([], RequestHeaders) ->
 +    RequestHeaders;
 +maybe_apply_cors_headers(CorsHeaders, RequestHeaders0) ->
 +    % for each RequestHeader that isn't in SimpleHeaders,
 +    % (or Content-Type with SIMPLE_CONTENT_TYPE_VALUES)
 +    % append to Access-Control-Expose-Headers
 +    % return: RequestHeaders ++ CorsHeaders ++ ACEH
 +
 +    RequestHeaders = [K || {K,_V} <- RequestHeaders0],
-     ExposedHeaders0 = reduce_headers(RequestHeaders, ?SIMPLE_HEADERS),
++    ExposedHeaders0 = reduce_headers(RequestHeaders, ?ALLOWED_HEADERS),
 +
 +    % here we may have not moved Content-Type into ExposedHeaders,
 +    % now we need to check whether the Content-Type valus is
 +    % in ?SIMPLE_CONTENT_TYPE_VALUES and if it isn’t add Content-
 +    % Type to to ExposedHeaders
-     ContentType = string:to_lower(
-         proplists:get_value("Content-Type", RequestHeaders0)),
- 
-     IncludeContentType = lists:member(ContentType, ?SIMPLE_CONTENT_TYPE_VALUES),
++    ContentType =  proplists:get_value("Content-Type", RequestHeaders0),
++    IncludeContentType = case ContentType of
++    undefined ->
++        false;
++    _ ->
++        ContentType_ = string:to_lower(ContentType),
++        lists:member(ContentType_, ?SIMPLE_CONTENT_TYPE_VALUES)
++    end,
 +    ExposedHeaders = case IncludeContentType of
 +    false ->
 +        lists:umerge(ExposedHeaders0, ["Content-Type"]);
 +    true ->
 +        ExposedHeaders0
 +    end,
 +    CorsHeaders
 +    ++ RequestHeaders0
 +    ++ [{"Access-Control-Expose-Headers",
 +            string:join(ExposedHeaders, ", ")}].
 +
 +
 +reduce_headers(A, B) ->
 +    reduce_headers0(A, B, []).
 +
 +reduce_headers0([], _B, Result) ->
-     Result;
++    lists:sort(Result);
 +reduce_headers0([ElmA|RestA], B, Result) ->
 +    R = case member_nocase(ElmA, B) of
-     true -> Result;
++    false -> Result;
 +    _Else -> [ElmA | Result]
 +    end,
 +    reduce_headers0(RestA, B, R).
 +
 +member_nocase(ElmA, List) ->
 +    lists:any(fun(ElmB) ->
 +        string:to_lower(ElmA) =:= string:to_lower(ElmB)
 +    end, List).
 +
 +handle_cors_headers(_Origin, _Host, []) ->
 +    [];
 +handle_cors_headers(Origin, Host, AcceptedOrigins) ->
 +    AcceptAll = lists:member("*", AcceptedOrigins),
 +    case {AcceptAll, lists:member(Origin, AcceptedOrigins)} of
 +    {true, _} ->
 +        make_cors_header(Origin, Host);
 +    {false, true}  ->
 +        make_cors_header(Origin, Host);
 +    _ ->
 +        % If the value of the Origin header is not a
 +        % case-sensitive match for any of the values
 +        % in list of origins, do not set any additional
 +        % headers and terminate this set of steps.
 +        % http://www.w3.org/TR/cors/#resource-requests
 +        []
 +    end.
 +
 +
 +make_cors_header(Origin, Host) ->
 +    Headers = [{"Access-Control-Allow-Origin", Origin}],
 +    maybe_add_credentials(Origin, Host, Headers).
 +
 +
 +%% util
 +
 +maybe_add_credentials(Origin, Host, Headers) ->
 +    maybe_add_credentials(Headers, allow_credentials(Origin, Host)).
 +
 +maybe_add_credentials(Headers, false) ->
 +    Headers;
 +maybe_add_credentials(Headers, true) ->
 +    Headers ++ [{"Access-Control-Allow-Credentials", "true"}].
 +
 +
 +allow_credentials("*", _Host) ->
 +    false;
 +allow_credentials(_Origin, Host) ->
 +    Default = get_bool_config("cors", "credentials", false),
 +    get_bool_config(cors_section(Host), "credentials", Default).
 +
 +
 +
 +cors_config(Host, Key, Default) ->
 +    config:get(cors_section(Host), Key,
 +                     config:get("cors", Key, Default)).
 +
 +cors_section(Host0) ->
 +    {Host, _Port} = split_host_port(Host0),
 +    "cors:" ++ Host.
 +
 +enable_cors() ->
 +    get_bool_config("httpd", "enable_cors", false).
 +
 +get_bool_config(Section, Key, Default) ->
 +    case config:get(Section, Key) of
 +    undefined ->
 +        Default;
 +    "true" ->
 +        true;
 +    "false" ->
 +        false
 +    end.
 +
 +get_accepted_origins(Host) ->
 +    split_list(cors_config(Host, "origins", [])).
 +
 +split_list(S) ->
 +    re:split(S, "\\s*,\\s*", [trim, {return, list}]).
 +
 +split_headers(H) ->
 +    re:split(H, ",\\s*", [{return,list}, trim]).
 +
 +split_host_port(HostAsString) ->
 +    % split at semicolon ":"
 +    Split = string:rchr(HostAsString, $:),
 +    split_host_port(HostAsString, Split).
 +
 +split_host_port(HostAsString, 0) ->
 +    % no semicolon
 +    {HostAsString, '*'};
 +split_host_port(HostAsString, N) ->
 +    HostPart = string:substr(HostAsString, 1, N-1),
 +    % parse out port
 +    % is there a nicer way?
 +    case (catch erlang:list_to_integer(string:substr(HostAsString,
 +                    N+1, length(HostAsString)))) of
 +    {'EXIT', _} ->
 +        {HostAsString, '*'};
 +    Port ->
 +        {HostPart, Port}
 +    end.


Mime
View raw message