couchdb-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Paul Davis <paul.joseph.da...@gmail.com>
Subject Re: git commit: handle CORS. fix #COUCHDB-431
Date Thu, 01 Nov 2012 04:34:47 GMT
On Wed, Oct 31, 2012 at 8:14 PM, Adam Kocoloski <kocolosk@apache.org> wrote:
> Right, the wiki page for this stuff is http://wiki.apache.org/couchdb/Merge_Procedure which now reads
>
>> Please use the ticket number, the type of the branch, along with a very short descriptive phrase, for your branch name.
>>
>> If the ticket was COUCHDB-1234, and the ticket title was My Cool Feature, your branch should be called 1234-feature-cool. If the issue is a bug and the branch includes the bug fix, it should be called 1234-fix-cool.
>
> Perhaps we should kill this branch and re-upload to follow the naming scheme?  Cheers,
>

I think there's a git syntax for renaming on a remote.

> Adam
>
> On Oct 31, 2012, at 7:53 PM, Benoit Chesneau <bchesneau@gmail.com> wrote:
>
>> hrmmmm i thought it was ticketnumber_shortdescr.... I didn't read last
>> update of the wiki though ..
>>
>> - benoƮt
>>
>>
>> On Thu, Nov 1, 2012 at 12:50 AM, Adam Kocoloski <kocolosk@apache.org> wrote:
>>
>>> A minor thing -- didn't we just propose earlier today to use a naming
>>> convention like 431-feature-CORS for these topic branches?
>>>
>>> Adam
>>>
>>> On Oct 31, 2012, at 7:43 PM, benoitc@apache.org wrote:
>>>
>>>> Updated Branches:
>>>> refs/heads/COUCHDB-431_cors [created] 0777262fa
>>>>
>>>>
>>>> handle CORS. fix #COUCHDB-431
>>>>
>>>> This patch as support of CORS requests and preflights request as a node
>>>> level. vhosts are supported
>>>>
>>>>
>>>> Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo
>>>> Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/0777262f
>>>> Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/0777262f
>>>> Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/0777262f
>>>>
>>>> Branch: refs/heads/COUCHDB-431_cors
>>>> Commit: 0777262fa291a79555ea23f2ff203d1ae7654547
>>>> Parents: 88c52b2
>>>> Author: benoitc <bchesneau@gmail.com>
>>>> Authored: Thu Nov 1 00:41:00 2012 +0100
>>>> Committer: benoitc <bchesneau@gmail.com>
>>>> Committed: Thu Nov 1 00:41:00 2012 +0100
>>>>
>>>> ----------------------------------------------------------------------
>>>> etc/couchdb/default.ini.tpl.in    |   23 +++-
>>>> src/couchdb/Makefile.am           |    4 +-
>>>> src/couchdb/couch_httpd.erl       |   53 ++++++--
>>>> src/couchdb/couch_httpd_cors.erl  |  230 ++++++++++++++++++++++++++++++++
>>>> src/couchdb/couch_httpd_vhost.erl |   55 ++++----
>>>> test/etap/231_cors.t              |  230 ++++++++++++++++++++++++++++++++
>>>> 6 files changed, 553 insertions(+), 42 deletions(-)
>>>> ----------------------------------------------------------------------
>>>>
>>>>
>>>>
>>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/etc/couchdb/default.ini.tpl.in
>>>> ----------------------------------------------------------------------
>>>> diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/
>>> default.ini.tpl.in
>>>> index 79ece5c..6a32f65 100644
>>>> --- a/etc/couchdb/default.ini.tpl.in
>>>> +++ b/etc/couchdb/default.ini.tpl.in
>>>> @@ -49,6 +49,7 @@ allow_jsonp = false
>>>> ; For more socket options, consult Erlang's module 'inet' man page.
>>>> ;socket_options = [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, true}]
>>>> log_max_chunk_size = 1000000
>>>> +cors_enable = false
>>>>
>>>> [ssl]
>>>> port = 6984
>>>> @@ -67,6 +68,26 @@ auth_cache_size = 50 ; size is number of cache entries
>>>> allow_persistent_cookies = false ; set to true to allow persistent
>>> cookies
>>>> iterations = 10000 ; iterations for password hashing
>>>>
>>>> +[cors]
>>>> +allows_credentials = false
>>>> +; List of origins separated by a comma
>>>> +;origins =
>>>> +; List of accepted headers separated by a comma
>>>> +; headers =
>>>> +; List of accepted methods
>>>> +; methods =
>>>> +
>>>> +
>>>> +; Configuration for a vhost
>>>> +:[cors:example.com]
>>>> +; allows_credentials = false
>>>> +; List of origins separated by a comma
>>>> +;origins =
>>>> +; List of accepted headers separated by a comma
>>>> +; headers =
>>>> +; List of accepted methods
>>>> +; methods =
>>>> +
>>>> [couch_httpd_oauth]
>>>> ; If set to 'true', oauth token and consumer secrets will be looked up
>>>> ; in the authentication database (_users). These secrets are stored in
>>>> @@ -224,7 +245,7 @@ socket_options = [{keepalive, true}, {nodelay,
>>> false}]
>>>> ;cert_file = /full/path/to/server_cert.pem
>>>> ; Path to file containing user's private PEM encoded key.
>>>> ;key_file = /full/path/to/server_key.pem
>>>> -; String containing the user's password. Only used if the private
>>> keyfile is password protected.
>>>> +; String containing the user's password. Only used if the private
>>> keyfile is password protected.
>>>> ;password = somepassword
>>>> ; Set to true to validate peer certificates.
>>>> verify_ssl_certificates = false
>>>>
>>>>
>>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/Makefile.am
>>>> ----------------------------------------------------------------------
>>>> diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am
>>>> index 5705976..9fe19bc 100644
>>>> --- a/src/couchdb/Makefile.am
>>>> +++ b/src/couchdb/Makefile.am
>>>> @@ -49,6 +49,7 @@ source_files = \
>>>>    couch_httpd.erl \
>>>>    couch_httpd_db.erl \
>>>>    couch_httpd_auth.erl \
>>>> +    couch_httpd_cors.erl \
>>>>    couch_httpd_oauth.erl \
>>>>    couch_httpd_external.erl \
>>>>    couch_httpd_misc_handlers.erl \
>>>> @@ -79,7 +80,7 @@ source_files = \
>>>>    couch_work_queue.erl \
>>>>    json_stream_parse.erl
>>>>
>>>> -EXTRA_DIST = $(source_files) couch_db.hrl couch_js_functions.hrl
>>>> +EXTRA_DIST = $(source_files) couch_db.hrl couch_js_functions.hrl
>>>>
>>>> compiled_files = \
>>>>    couch.app \
>>>> @@ -106,6 +107,7 @@ compiled_files = \
>>>>    couch_httpd_db.beam \
>>>>    couch_httpd_auth.beam \
>>>>    couch_httpd_oauth.beam \
>>>> +    couch_httpd_cors.beam \
>>>>    couch_httpd_proxy.beam \
>>>>    couch_httpd_external.beam \
>>>>    couch_httpd_misc_handlers.beam \
>>>>
>>>>
>>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd.erl
>>>> ----------------------------------------------------------------------
>>>> diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl
>>>> index 45ceebc..6bba871 100644
>>>> --- a/src/couchdb/couch_httpd.erl
>>>> +++ b/src/couchdb/couch_httpd.erl
>>>> @@ -275,7 +275,10 @@ handle_request_int(MochiReq, DefaultFun,
>>>>
>>>>    % 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
>>>> +    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
>>>> @@ -312,13 +315,19 @@ handle_request_int(MochiReq, DefaultFun,
>>>>    HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers,
>>> DefaultFun),
>>>>    {ok, AuthHandlers} = application:get_env(couch, auth_handlers),
>>>>
>>>> +    ?LOG_INFO("fuck you ~p~n", [Method]),
>>>>    {ok, Resp} =
>>>>    try
>>>> -        case authenticate_request(HttpReq, AuthHandlers) of
>>>> -        #httpd{} = Req ->
>>>> -            HandlerFun(Req);
>>>> -        Response ->
>>>> -            Response
>>>> +        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} ->
>>>> @@ -450,10 +459,13 @@ accepted_encodings(#httpd{mochi_req=MochiReq}) ->
>>>> serve_file(Req, RelativePath, DocumentRoot) ->
>>>>    serve_file(Req, RelativePath, DocumentRoot, []).
>>>>
>>>> -serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot,
>>> ExtraHeaders) ->
>>>> +serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot,
>>>> +           ExtraHeaders) ->
>>>>    log_request(Req, 200),
>>>> -    {ok, MochiReq:serve_file(RelativePath, DocumentRoot,
>>>> -        server_header() ++ couch_httpd_auth:cookie_auth_header(Req, [])
>>> ++ ExtraHeaders)}.
>>>> +    {ok, MochiReq:serve_file(RelativePath, DocumentRoot,
>>> server_header() ++
>>>> +                             couch_httpd_cors:cors_headers(Req) ++
>>>> +                             couch_httpd_auth:cookie_auth_header(Req,
>>> []) ++
>>>> +                             ExtraHeaders)}.
>>>>
>>>> qs_value(Req, Key) ->
>>>>    qs_value(Req, Key, undefined).
>>>> @@ -603,7 +615,10 @@ log_request(#httpd{mochi_req=MochiReq,peer=Peer},
>>> Code) ->
>>>> start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers,
>>> Length) ->
>>>>    log_request(Req, Code),
>>>>    couch_stats_collector:increment({httpd_status_codes, Code}),
>>>> -    Resp = MochiReq:start_response_length({Code, Headers ++
>>> server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers),
>>> Length}),
>>>> +    Headers1 = Headers ++ server_header() ++
>>>> +               couch_httpd_auth:cookie_auth_header(Req, Headers) ++
>>>> +               couch_httpd_cors:cors_headers(Req),
>>>> +    Resp = MochiReq:start_response_length({Code, Headers1, Length}),
>>>>    case MochiReq:get(method) of
>>>>    'HEAD' -> throw({http_head_abort, Resp});
>>>>    _ -> ok
>>>> @@ -614,7 +629,8 @@ 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),
>>>> -    Headers2 = Headers ++ server_header() ++ CookieHeader,
>>>> +    Headers2 = Headers ++ server_header() ++ CookieHeader ++
>>>> +               couch_httpd_cors:cors_headers(Req),
>>>>    Resp = MochiReq:start_response({Code, Headers2}),
>>>>    case MochiReq:get(method) of
>>>>        'HEAD' -> throw({http_head_abort, Resp});
>>>> @@ -646,8 +662,11 @@ http_1_0_keep_alive(Req, Headers) ->
>>>> start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
>>>>    log_request(Req, Code),
>>>>    couch_stats_collector:increment({httpd_status_codes, Code}),
>>>> -    Headers2 = http_1_0_keep_alive(MochiReq, Headers),
>>>> -    Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++
>>> couch_httpd_auth:cookie_auth_header(Req, Headers2), chunked}),
>>>> +    Headers1 = http_1_0_keep_alive(MochiReq, Headers),
>>>> +    Headers2 = Headers1 ++ server_header() ++
>>>> +               couch_httpd_auth:cookie_auth_header(Req, Headers1) ++
>>>> +               couch_httpd_cors:cors_headers(Req),
>>>> +    Resp = MochiReq:respond({Code, Headers2, chunked}),
>>>>    case MochiReq:get(method) of
>>>>    'HEAD' -> throw({http_head_abort, Resp});
>>>>    _ -> ok
>>>> @@ -668,14 +687,18 @@ last_chunk(Resp) ->
>>>> send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) ->
>>>>    log_request(Req, Code),
>>>>    couch_stats_collector:increment({httpd_status_codes, Code}),
>>>> -    Headers2 = http_1_0_keep_alive(MochiReq, Headers),
>>>> +    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,
>>>> -    {ok, MochiReq:respond({Code, Headers2 ++ server_header() ++
>>> couch_httpd_auth:cookie_auth_header(Req, Headers2), Body})}.
>>>> +    Headers2 = Headers1 ++ server_header() ++
>>>> +               couch_httpd_cors:cors_headers(Req) ++
>>>> +               couch_httpd_auth:cookie_auth_header(Req, Headers1),
>>>> +
>>>> +    {ok, MochiReq:respond({Code, Headers2, Body})}.
>>>>
>>>> send_method_not_allowed(Req, Methods) ->
>>>>    send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>,
>>> ?l2b("Only " ++ Methods ++ " allowed")).
>>>>
>>>>
>>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd_cors.erl
>>>> ----------------------------------------------------------------------
>>>> diff --git a/src/couchdb/couch_httpd_cors.erl
>>> b/src/couchdb/couch_httpd_cors.erl
>>>> new file mode 100644
>>>> index 0000000..69f57ed
>>>> --- /dev/null
>>>> +++ b/src/couchdb/couch_httpd_cors.erl
>>>> @@ -0,0 +1,230 @@
>>>> +% 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.
>>>> +
>>>> +
>>>> +-module(couch_httpd_cors).
>>>> +
>>>> +-include("couch_db.hrl").
>>>> +
>>>> +-export([is_preflight_request/1, cors_headers/1]).
>>>> +
>>>> +-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").
>>>> +
>>>> +is_preflight_request(#httpd{method=Method}=Req) when Method /=
>>> 'OPTIONS' ->
>>>> +    Req;
>>>> +is_preflight_request(#httpd{mochi_req=MochiReq}=Req) ->
>>>> +    case get_bool_config("httpd", "enable_cors", false) of
>>>> +        true ->
>>>> +            case preflight_request(MochiReq) of
>>>> +                {ok, PreflightHeaders} ->
>>>> +                    couch_httpd:send_response(Req, 204,
>>> PreflightHeaders, <<>>);
>>>> +                _ ->
>>>> +                    Req
>>>> +            end;
>>>> +        false ->
>>>> +            Req
>>>> +    end.
>>>> +
>>>> +
>>>> +cors_headers(#httpd{mochi_req=MochiReq}) ->
>>>> +    Host = couch_httpd_vhost:host(MochiReq),
>>>> +    case get_bool_config("httpd", "enable_cors", false) of
>>>> +        true ->
>>>> +            AcceptedOrigins = re:split(cors_config(Host, "origins", []),
>>>> +                                       "\\s*,\\s*",
>>>> +                                       [trim, {return, list}]),
>>>> +            case MochiReq:get_header_value("Origin") of
>>>> +                undefined ->
>>>> +                    [];
>>>> +                <<"*">> ->
>>>> +                    handle_cors_headers("*", Host, AcceptedOrigins);
>>>> +                <<"null">> ->
>>>> +                    handle_cors_headers("*", Host, AcceptedOrigins);
>>>> +                Origin ->
>>>> +                    handle_cors_headers(couch_util:to_list(Origin),
>>>> +                                        Host, AcceptedOrigins)
>>>> +            end;
>>>> +        false ->
>>>> +            []
>>>> +    end.
>>>> +
>>>> +handle_cors_headers("*", _Host, _AcceptedOrigins) ->
>>>> +    [{"Access-Control-Allow-Origin", "*"}];
>>>> +handle_cors_headers(Origin, Host, []) ->
>>>> +    case allows_credentials(Origin, Host) of
>>>> +        true ->
>>>> +            [{"Access-Control-Allow-Origin", Origin},
>>>> +             {"Access-Control-Allow-Credentials", "true"}];
>>>> +        false ->
>>>> +            [{"Access-Control-Allow-Origin", Origin}]
>>>> +    end;
>>>> +handle_cors_headers(Origin, Host, AcceptedOrigins) ->
>>>> +    AllowsCredentials = allows_credentials(Origin, Host),
>>>> +    case lists:member(Origin, AcceptedOrigins) of
>>>> +        true when AllowsCredentials =:= true ->
>>>> +            [{"Access-Control-Allow-Origin", Origin},
>>>> +             {"Access-Control-Allow-Credentials", "true"}];
>>>> +        true ->
>>>> +            [{"Access-Control-Allow-Origin", Origin}];
>>>> +        _ ->
>>>> +            []
>>>> +    end.
>>>> +
>>>> +
>>>> +preflight_request(MochiReq) ->
>>>> +    Host = couch_httpd_vhost:host(MochiReq),
>>>> +    case MochiReq:get_header_value("Origin") of
>>>> +        undefined ->
>>>> +            MochiReq;
>>>> +        <<"*">>  ->
>>>> +            handle_preflight_request("*", Host, MochiReq);
>>>> +        <<"null">> ->
>>>> +            handle_preflight_request("*", Host, MochiReq);
>>>> +        Origin ->
>>>> +            AcceptedOrigins = re:split(cors_config(Host, "origins", []),
>>>> +                                       "\\s*,\\s*",
>>>> +                                       [trim, {return, list}]),
>>>> +            case AcceptedOrigins of
>>>> +                [] ->
>>>> +                    handle_preflight_request(couch_util:to_list(Origin),
>>>> +                                             Host, MochiReq);
>>>> +                _ ->
>>>> +                    case lists:member(Origin, AcceptedOrigins) of
>>>> +                        true ->
>>>> +
>>> handle_preflight_request(couch_util:to_list(Origin),
>>>> +                                                     Host, MochiReq);
>>>> +                        false ->
>>>> +                            false
>>>> +                    end
>>>> +            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", "12345"),
>>>> +
>>>> +    PreflightHeaders0 = case allows_credentials(Origin, Host) of
>>>> +        true ->
>>>> +            [{"Access-Control-Allow-Origin", Origin},
>>>> +             {"Access-Control-Allow-Credentials", "true"},
>>>> +             {"Access-Control-Max-Age", MaxAge},
>>>> +             {"Access-Control-Allow-Methods",
>>> string:join(SupportedMethods,
>>>> +                                                          ", ")}];
>>>> +        false ->
>>>> +            [{"Access-Control-Allow-Origin", Origin},
>>>> +             {"Access-Control-Max-Age", MaxAge},
>>>> +             {"Access-Control-Allow-Methods",
>>> string:join(SupportedMethods,
>>>> +                                                          ", ")}]
>>>> +    end,
>>>> +
>>>> +    case MochiReq:get_header_value("Access-Control-Request-Method") of
>>>> +        undefined ->
>>>> +            {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 <- re:split(Headers, ",\\s*",
>>>> +
>>> [{return,list},trim])],
>>>> +                            {Headers, RH}
>>>> +                    end,
>>>> +                    % check if headers are supported
>>>> +                    case ReqHeaders -- SupportedHeaders of
>>>> +                        [] ->
>>>> +                            PreflightHeaders = PreflightHeaders0 ++
>>>> +
>>> [{"Access-Control-Allow-Headers",
>>>> +                                                 FinalReqHeaders}],
>>>> +                            {ok, PreflightHeaders};
>>>> +                        _ ->
>>>> +                            false
>>>> +                    end;
>>>> +                false ->
>>>> +                    false
>>>> +            end
>>>> +    end.
>>>> +
>>>> +
>>>> +allows_credentials("*", _Host) ->
>>>> +    false;
>>>> +allows_credentials(_Origin, Host) ->
>>>> +    Default = get_bool_config("cors", "allows_credentials",
>>>> +                              false),
>>>> +
>>>> +    get_bool_config(cors_section(Host), "allows_credentials",
>>>> +                    Default).
>>>> +
>>>> +
>>>> +cors_config(Host, Key, Default) ->
>>>> +    couch_config:get(cors_section(Host), Key,
>>>> +                     couch_config:get("cors", Key, Default)).
>>>> +
>>>> +cors_section(Host0) ->
>>>> +    {Host, _Port} = split_host_port(Host0),
>>>> +    "cors:" ++ Host.
>>>> +
>>>> +get_bool_config(Section, Key, Default) ->
>>>> +    case couch_config:get(Section, Key) of
>>>> +        undefined ->
>>>> +            Default;
>>>> +        "true" ->
>>>> +            true;
>>>> +        "false" ->
>>>> +            false
>>>> +    end.
>>>> +
>>>> +split_list(S) ->
>>>> +    re:split(S, "\\s*,\\s*", [trim, {return, list}]).
>>>> +
>>>> +split_host_port(HostAsString) ->
>>>> +    case string:rchr(HostAsString, $:) of
>>>> +        0 ->
>>>> +            {HostAsString, '*'};
>>>> +        N ->
>>>> +            HostPart = string:substr(HostAsString, 1, N-1),
>>>> +            case (catch
>>> erlang:list_to_integer(string:substr(HostAsString,
>>>> +                            N+1, length(HostAsString)))) of
>>>> +                {'EXIT', _} ->
>>>> +                    {HostAsString, '*'};
>>>> +                Port ->
>>>> +                    {HostPart, Port}
>>>> +            end
>>>> +    end.
>>>>
>>>>
>>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd_vhost.erl
>>>> ----------------------------------------------------------------------
>>>> diff --git a/src/couchdb/couch_httpd_vhost.erl
>>> b/src/couchdb/couch_httpd_vhost.erl
>>>> index 59f05ce..4c3ebfe 100644
>>>> --- a/src/couchdb/couch_httpd_vhost.erl
>>>> +++ b/src/couchdb/couch_httpd_vhost.erl
>>>> @@ -15,7 +15,7 @@
>>>>
>>>> -export([start_link/0, config_change/2, reload/0, get_state/0,
>>> dispatch_host/1]).
>>>> -export([urlsplit_netloc/2, redirect_to_vhost/2]).
>>>> -
>>>> +-export([host/1, split_host_port/1]).
>>>>
>>>> -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
>>> terminate/2, code_change/3]).
>>>>
>>>> @@ -32,7 +32,7 @@
>>>> %% doc the vhost manager.
>>>> %% This gen_server keep state of vhosts added to the ini and try to
>>>> %% match the Host header (or forwarded) against rules built against
>>>> -%% vhost list.
>>>> +%% vhost list.
>>>> %%
>>>> %% Declaration of vhosts take place in the configuration file :
>>>> %%
>>>> @@ -51,7 +51,7 @@
>>>> %%      "*.db.example.com = /"  will match all cname on top of db
>>>> %% examples to the root of the machine.
>>>> %%
>>>> -%%
>>>> +%%
>>>> %% Rewriting Hosts to path
>>>> %% -----------------------
>>>> %%
>>>> @@ -75,7 +75,7 @@
>>>> %%    redirect_vhost_handler = {Module, Fun}
>>>> %%
>>>> %% The function take 2 args : the mochiweb request object and the target
>>>> -%%% path.
>>>> +%%% path.
>>>>
>>>> start_link() ->
>>>>    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
>>>> @@ -98,15 +98,7 @@ dispatch_host(MochiReq) ->
>>>>    {"/" ++ VPath, Query, Fragment} =
>>> mochiweb_util:urlsplit_path(MochiReq:get(raw_path)),
>>>>    VPathParts =  string:tokens(VPath, "/"),
>>>>
>>>> -    XHost = couch_config:get("httpd", "x_forwarded_host",
>>> "X-Forwarded-Host"),
>>>> -    VHost = case MochiReq:get_header_value(XHost) of
>>>> -        undefined ->
>>>> -            case MochiReq:get_header_value("Host") of
>>>> -                undefined -> [];
>>>> -                Value1 -> Value1
>>>> -            end;
>>>> -        Value -> Value
>>>> -    end,
>>>> +    VHost = host(MochiReq),
>>>>    {VHostParts, VhostPort} = split_host_port(VHost),
>>>>    FinalMochiReq = case try_bind_vhost(VHosts,
>>> lists:reverse(VHostParts),
>>>>            VhostPort, VPathParts) of
>>>> @@ -133,14 +125,14 @@ append_path("/"=_Target, "/"=_Path) ->
>>>> append_path(Target, Path) ->
>>>>    Target ++ Path.
>>>>
>>>> -% default redirect vhost handler
>>>> +% default redirect vhost handler
>>>> redirect_to_vhost(MochiReq, VhostTarget) ->
>>>>    Path = MochiReq:get(raw_path),
>>>>    Target = append_path(VhostTarget, Path),
>>>>
>>>>    ?LOG_DEBUG("Vhost Target: '~p'~n", [Target]),
>>>>
>>>> -    Headers = mochiweb_headers:enter("x-couchdb-vhost-path", Path,
>>>> +    Headers = mochiweb_headers:enter("x-couchdb-vhost-path", Path,
>>>>        MochiReq:get(headers)),
>>>>
>>>>    % build a new mochiweb request
>>>> @@ -154,7 +146,7 @@ redirect_to_vhost(MochiReq, VhostTarget) ->
>>>>    MochiReq1.
>>>>
>>>> %% if so, then it will not be rewritten, but will run as a normal
>>> couchdb request.
>>>> -%* normally you'd use this for _uuids _utils and a few of the others
>>> you want to
>>>> +%* normally you'd use this for _uuids _utils and a few of the others
>>> you want to
>>>> %% keep available on vhosts. You can also use it to make databases
>>> 'global'.
>>>> vhost_global( VhostGlobals, MochiReq) ->
>>>>    RawUri = MochiReq:get(raw_path),
>>>> @@ -175,14 +167,14 @@ try_bind_vhost([], _HostParts, _Port, _PathParts)
>>> ->
>>>> try_bind_vhost([VhostSpec|Rest], HostParts, Port, PathParts) ->
>>>>    {{VHostParts, VPort, VPath}, Path} = VhostSpec,
>>>>    case bind_port(VPort, Port) of
>>>> -        ok ->
>>>> +        ok ->
>>>>            case bind_vhost(lists:reverse(VHostParts), HostParts, []) of
>>>>                {ok, Bindings, Remainings} ->
>>>>                    case bind_path(VPath, PathParts) of
>>>>                        {ok, PathParts1} ->
>>>>                            Path1 = make_target(Path, Bindings,
>>> Remainings, []),
>>>>                            {make_path(Path1), make_path(PathParts1)};
>>>> -                        fail ->
>>>> +                        fail ->
>>>>                            try_bind_vhost(Rest, HostParts, Port,
>>>>                                PathParts)
>>>>                    end;
>>>> @@ -193,7 +185,7 @@ try_bind_vhost([VhostSpec|Rest], HostParts, Port,
>>> PathParts) ->
>>>>
>>>> %% doc: build new patch from bindings. bindings are query args
>>>> %% (+ dynamic query rewritten if needed) and bindings found in
>>>> -%% bind_path step.
>>>> +%% bind_path step.
>>>> %% TODO: merge code with rewrite. But we need to make sure we are
>>>> %% in string here.
>>>> make_target([], _Bindings, _Remaining, Acc) ->
>>>> @@ -223,7 +215,7 @@ bind_vhost([],[], Bindings) -> {ok, Bindings, []};
>>>> bind_vhost([?MATCH_ALL], [], _Bindings) -> fail;
>>>> bind_vhost([?MATCH_ALL], Rest, Bindings) -> {ok, Bindings, Rest};
>>>> bind_vhost([], _HostParts, _Bindings) -> fail;
>>>> -bind_vhost([{bind, Token}|Rest], [Match|RestHost], Bindings) ->
>>>> +bind_vhost([{bind, Token}|Rest], [Match|RestHost], Bindings) ->
>>>>    bind_vhost(Rest, RestHost, [{{bind, Token}, Match}|Bindings]);
>>>> bind_vhost([Cname|Rest], [Cname|RestHost], Bindings) ->
>>>>    bind_vhost(Rest, RestHost, Bindings);
>>>> @@ -243,6 +235,19 @@ bind_path(_, _) ->
>>>>
>>>>
>>>> %% create vhost list from ini
>>>> +
>>>> +host(MochiReq) ->
>>>> +    XHost = couch_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 -> [];
>>>> +                Value1 -> Value1
>>>> +            end;
>>>> +        Value -> Value
>>>> +    end.
>>>> +
>>>> make_vhosts() ->
>>>>    Vhosts = lists:foldl(fun
>>>>                ({_, ""}, Acc) ->
>>>> @@ -267,15 +272,15 @@ parse_vhost(Vhost) ->
>>>>            H1 = make_spec(H, []),
>>>>            {H1, P, string:tokens(Path, "/")}
>>>>    end.
>>>> -
>>>> +
>>>>
>>>> split_host_port(HostAsString) ->
>>>>    case string:rchr(HostAsString, $:) of
>>>>        0 ->
>>>>            {split_host(HostAsString), '*'};
>>>>        N ->
>>>> -            HostPart = string:substr(HostAsString, 1, N-1),
>>>> -            case (catch
>>> erlang:list_to_integer(string:substr(HostAsString,
>>>> +            HostPart = string:substr(HostAsString, 1, N-1),
>>>> +            case (catch
>>> erlang:list_to_integer(string:substr(HostAsString,
>>>>                            N+1, length(HostAsString)))) of
>>>>                {'EXIT', _} ->
>>>>                    {split_host(HostAsString), '*'};
>>>> @@ -303,7 +308,7 @@ make_spec([P|R], Acc) ->
>>>>
>>>>
>>>> parse_var(P) ->
>>>> -    case P of
>>>> +    case P of
>>>>        ":" ++ Var ->
>>>>            {bind, Var};
>>>>        _ -> P
>>>> @@ -323,7 +328,7 @@ make_path(Parts) ->
>>>>
>>>> init(_) ->
>>>>    ok = couch_config:register(fun ?MODULE:config_change/2),
>>>> -
>>>> +
>>>>    %% load configuration
>>>>    {VHostGlobals, VHosts, Fun} = load_conf(),
>>>>    State = #vhosts_state{
>>>>
>>>>
>>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/test/etap/231_cors.t
>>>> ----------------------------------------------------------------------
>>>> diff --git a/test/etap/231_cors.t b/test/etap/231_cors.t
>>>> new file mode 100644
>>>> index 0000000..72fc3df
>>>> --- /dev/null
>>>> +++ b/test/etap/231_cors.t
>>>> @@ -0,0 +1,230 @@
>>>> +#!/usr/bin/env escript
>>>> +%% -*- erlang -*-
>>>> +
>>>> +% 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.
>>>> +
>>>> +-record(user_ctx, {
>>>> +    name = null,
>>>> +    roles = [],
>>>> +    handler
>>>> +}).
>>>> +
>>>> +
>>>> +-define(SUPPORTED_METHODS, "GET, HEAD, POST, PUT, DELETE, TRACE,
>>> CONNECT, COPY, OPTIONS").
>>>> +server() ->
>>>> +    lists:concat([
>>>> +        "http://127.0.0.1:",
>>>> +        mochiweb_socket_server:get(couch_httpd, port),
>>>> +        "/"
>>>> +    ]).
>>>> +
>>>> +
>>>> +main(_) ->
>>>> +    test_util:init_code_path(),
>>>> +
>>>> +    etap:plan(11),
>>>> +    case (catch test()) of
>>>> +        ok ->
>>>> +            etap:end_tests();
>>>> +        Other ->
>>>> +            etap:diag(io_lib:format("Test died abnormally: ~p",
>>> [Other])),
>>>> +            etap:bail(Other)
>>>> +    end,
>>>> +    ok.
>>>> +
>>>> +dbname() -> "etap-test-db".
>>>> +dbname1() -> "etap-test-db1".
>>>> +dbname2() -> "etap-test-db2".
>>>> +
>>>> +admin_user_ctx() -> {user_ctx, #user_ctx{roles=[<<"_admin">>]}}.
>>>> +
>>>> +set_admin_password(UserName, Password) ->
>>>> +    Salt = binary_to_list(couch_uuids:random()),
>>>> +    Hashed = couch_util:to_hex(crypto:sha(Password ++ Salt)),
>>>> +    couch_config:set("admins", UserName,
>>>> +        "-hashed-" ++ Hashed ++ "," ++ Salt, false).
>>>> +
>>>> +test() ->
>>>> +
>>>> +    ibrowse:start(),
>>>> +    crypto:start(),
>>>> +
>>>> +    %% launch couchdb
>>>> +    couch_server_sup:start_link(test_util:config_files()),
>>>> +
>>>> +    %% initialize db
>>>> +    timer:sleep(1000),
>>>> +    couch_server:delete(list_to_binary(dbname()), [admin_user_ctx()]),
>>>> +    couch_server:delete(list_to_binary(dbname1()), [admin_user_ctx()]),
>>>> +    couch_server:delete(list_to_binary(dbname2()), [admin_user_ctx()]),
>>>> +    {ok, Db} = couch_db:create(list_to_binary(dbname()),
>>> [admin_user_ctx()]),
>>>> +    {ok, Db1} = couch_db:create(list_to_binary(dbname1()),
>>> [admin_user_ctx()]),
>>>> +    {ok, Db2} = couch_db:create(list_to_binary(dbname2()),
>>> [admin_user_ctx()]),
>>>> +
>>>> +    % CORS is disabled by default
>>>> +    test_no_headers_server(),
>>>> +    test_no_headers_db(),
>>>> +
>>>> +    % Now enable CORS
>>>> +    ok = couch_config:set("httpd", "enable_cors", "true", false),
>>>> +    ok = couch_config:set("cors", "origins", "http://example.com",
>>> false),
>>>> +
>>>> +    %% do tests
>>>> +    test_incorrect_origin_simple_request(),
>>>> +    test_incorrect_origin_preflight_request(),
>>>> +
>>>> +    test_preflight_request(),
>>>> +    test_db_request(),
>>>> +    test_db_preflight_request(),
>>>> +    test_db_origin_request(),
>>>> +    test_db1_origin_request(),
>>>> +
>>>> +    %% do tests with auth
>>>> +    ok = set_admin_password("test", "test"),
>>>> +
>>>> +    test_db_preflight_auth_request(),
>>>> +    test_db_origin_auth_request(),
>>>> +
>>>> +    %% restart boilerplate
>>>> +    catch couch_db:close(Db),
>>>> +    catch couch_db:close(Db1),
>>>> +    catch couch_db:close(Db2),
>>>> +
>>>> +    couch_server:delete(list_to_binary(dbname()), [admin_user_ctx()]),
>>>> +    couch_server:delete(list_to_binary(dbname1()), [admin_user_ctx()]),
>>>> +    couch_server:delete(list_to_binary(dbname2()), [admin_user_ctx()]),
>>>> +
>>>> +    timer:sleep(3000),
>>>> +    couch_server_sup:stop(),
>>>> +    ok.
>>>> +
>>>> +%% Cors is disabled, should not return Access-Control-Allow-Origin
>>>> +test_no_headers_server() ->
>>>> +    Headers = [{"Origin", "http://127.0.0.1"}],
>>>> +    {ok, _, Resp, _} = ibrowse:send_req(server(), Headers, get, []),
>>>> +    etap:is(proplists:get_value("Access-Control-Allow-Origin", Resp),
>>>> +            undefined, "No CORS Headers when disabled").
>>>> +
>>>> +%% Cors is disabled, should not return Access-Control-Allow-Origin
>>>> +test_no_headers_db() ->
>>>> +    Headers = [{"Origin", "http://127.0.0.1"}],
>>>> +    Url = server() ++ "etap-test-db",
>>>> +    {ok, _, Resp, _} = ibrowse:send_req(Url, Headers, get, []),
>>>> +    etap:is(proplists:get_value("Access-Control-Allow-Origin", Resp),
>>>> +            undefined, "No CORS Headers when disabled").
>>>> +
>>>> +test_incorrect_origin_simple_request() ->
>>>> +    Headers = [{"Origin", "http://127.0.0.1"}],
>>>> +    {ok, _, RespHeaders, _} = ibrowse:send_req(server(), Headers, get,
>>> []),
>>>> +    etap:is(proplists:get_value("Access-Control-Allow-Origin",
>>> RespHeaders),
>>>> +            undefined,
>>>> +            "Specified invalid origin, no Access").
>>>> +
>>>> +test_incorrect_origin_preflight_request() ->
>>>> +    Headers = [{"Origin", "http://127.0.0.1"},
>>>> +               {"Access-Control-Request-Method", "GET"}],
>>>> +    {ok, _, RespHeaders, _} = ibrowse:send_req(server(), Headers,
>>> options, []),
>>>> +    etap:is(proplists:get_value("Access-Control-Allow-Origin",
>>> RespHeaders),
>>>> +            undefined,
>>>> +            "invalid origin").
>>>> +
>>>> +test_preflight_request() ->
>>>> +    Headers = [{"Origin", "http://example.com"},
>>>> +               {"Access-Control-Request-Method", "GET"}],
>>>> +    case ibrowse:send_req(server(), Headers, options, []) of
>>>> +    {ok, _, RespHeaders, _}  ->
>>>> +        etap:is(proplists:get_value("Access-Control-Allow-Methods",
>>> RespHeaders),
>>>> +            ?SUPPORTED_METHODS,
>>>> +            "test_preflight_request Access-Control-Allow-Methods ok");
>>>> +    _ ->
>>>> +        etap:is(false, true, "ibrowse failed")
>>>> +    end.
>>>> +
>>>> +test_db_request() ->
>>>> +    Headers = [{"Origin", "http://example.com"}],
>>>> +    Url = server() ++ "etap-test-db",
>>>> +    case ibrowse:send_req(Url, Headers, get, []) of
>>>> +    {ok, _, RespHeaders, _Body} ->
>>>> +        etap:is(proplists:get_value("Access-Control-Allow-Origin",
>>> RespHeaders),
>>>> +            "http://example.com",
>>>> +            "db Access-Control-Allow-Origin ok");
>>>> +    _ ->
>>>> +        etap:is(false, true, "ibrowse failed")
>>>> +    end.
>>>> +
>>>> +test_db_preflight_request() ->
>>>> +    Url = server() ++ "etap-test-db",
>>>> +    Headers = [{"Origin", "http://example.com"},
>>>> +               {"Access-Control-Request-Method", "GET"}],
>>>> +    case ibrowse:send_req(Url, Headers, options, []) of
>>>> +    {ok, _, RespHeaders, _} ->
>>>> +        etap:is(proplists:get_value("Access-Control-Allow-Methods",
>>> RespHeaders),
>>>> +                ?SUPPORTED_METHODS,
>>>> +                "db Access-Control-Allow-Methods ok");
>>>> +    _ ->
>>>> +        etap:is(false, true, "ibrowse failed")
>>>> +    end.
>>>> +
>>>> +
>>>> +test_db_origin_request() ->
>>>> +    Headers = [{"Origin", "http://example.com"}],
>>>> +    Url = server() ++ "etap-test-db",
>>>> +    case ibrowse:send_req(Url, Headers, get, []) of
>>>> +    {ok, _, RespHeaders, _Body} ->
>>>> +        etap:is(proplists:get_value("Access-Control-Allow-Origin",
>>> RespHeaders),
>>>> +            "http://example.com",
>>>> +            "db origin ok");
>>>> +    _ ->
>>>> +        etap:is(false, true, "ibrowse failed")
>>>> +    end.
>>>> +
>>>> +test_db1_origin_request() ->
>>>> +    Headers = [{"Origin", "http://example.com"}],
>>>> +    Url = server() ++ "etap-test-db1",
>>>> +    case ibrowse:send_req(Url, Headers, get, [], [{host_header, "
>>> example.com"}]) of
>>>> +    {ok, _, RespHeaders, _Body} ->
>>>> +        etap:is(proplists:get_value("Access-Control-Allow-Origin",
>>> RespHeaders),
>>>> +            "http://example.com",
>>>> +            "db origin ok");
>>>> +    _Else ->
>>>> +        io:format("else ~p~n", [_Else]),
>>>> +        etap:is(false, true, "ibrowse failed")
>>>> +    end.
>>>> +
>>>> +test_db_preflight_auth_request() ->
>>>> +    Url = server() ++ "etap-test-db2",
>>>> +    Headers = [{"Origin", "http://example.com"},
>>>> +               {"Access-Control-Request-Method", "GET"}],
>>>> +    case ibrowse:send_req(Url, Headers, options, []) of
>>>> +    {ok, _Status, RespHeaders, _} ->
>>>> +        etap:is(proplists:get_value("Access-Control-Allow-Methods",
>>> RespHeaders),
>>>> +                ?SUPPORTED_METHODS,
>>>> +                "db Access-Control-Allow-Methods ok");
>>>> +    _ ->
>>>> +        etap:is(false, true, "ibrowse failed")
>>>> +    end.
>>>> +
>>>> +
>>>> +test_db_origin_auth_request() ->
>>>> +    Headers = [{"Origin", "http://example.com"}],
>>>> +    Url = server() ++ "etap-test-db2",
>>>> +
>>>> +    case ibrowse:send_req(Url, Headers, get, [],
>>>> +        [{basic_auth, {"test", "test"}}]) of
>>>> +    {ok, _, RespHeaders, _Body} ->
>>>> +        etap:is(proplists:get_value("Access-Control-Allow-Origin",
>>> RespHeaders),
>>>> +            "http://example.com",
>>>> +            "db origin ok");
>>>> +    _ ->
>>>> +        etap:is(false, true, "ibrowse failed")
>>>> +    end.
>>>>
>>>
>>>
>

Mime
View raw message