You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@couchdb.apache.org by Adam Kocoloski <ko...@apache.org> on 2012/11/01 00:50:34 UTC

Re: git commit: handle CORS. fix #COUCHDB-431

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 <bc...@gmail.com>
> Authored: Thu Nov 1 00:41:00 2012 +0100
> Committer: benoitc <bc...@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.
> 


Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Benoit Chesneau <bc...@gmail.com>.
doh forget the last part. coffee is needed :)


On Thu, Nov 1, 2012 at 7:02 AM, Benoit Chesneau <bc...@gmail.com> wrote:

>
>
>
> On Thu, Nov 1, 2012 at 6:52 AM, Adam Kocoloski <ko...@apache.org>wrote:
>
>> On Nov 1, 2012, at 1:41 AM, Benoit Chesneau <bc...@gmail.com> wrote:
>>
>> > ok I will stop for now pushed 431_feature_cors (the removing of caps is
>> > wanted here and I will be happy to just update the jira title if someone
>> > really care) .
>>
>> Sure, I only used caps because CORS is an acronym.  It doesn't matter at
>> all.  Is there a reason why you keep using underscores instead of hyphens?
>>
>> > Side note: Imo adding feature in the naming here doesn't give anything
>>  .
>> > Prefixing by feature/ at least would optimize queries for rapid eye
>> looking
>> > or a bot going over the issues...
>>
>> Probably best to bring that up as an objection to Jan's proposal in the
>> "branching in couchdb" thread.  Personally I prefer the 431-feature-cors
>> style from that proposal.
>>
>
> I'm doing it now. The point of using feature/ and fix/ is to help later
> when we will all works actively on different features/ fixes/ and the
> number of branch will be larger. Using XXX-feature_... will not help at all
> in this case. I know this isn't a problem today but I prefer having a
> strict naming like this at first rather than trying to change things later.
> Especially since this is a known problem.
>
>
>
>> I realize I'm being incredibly pedantic, but it just rubbed me the wrong
>> way that we collectively wrote 9 emails on the topic branch naming today,
>> got what appeared to be lazy consensus on a convention, and then almost
>> immediately ignored that convention.  Regards,
>>
>
> That wasn't intended. I may have missed that we reached a consensus on a
> new naming. For hyphen and such well... I will fix it again, though i think
> using - helps to separate meanings when _ is normally used to extend a
> meaning of the same info (here ticket number vs short description) .
>
> - benoît
>

Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Benoit Chesneau <bc...@gmail.com>.
On Thu, Nov 1, 2012 at 6:52 AM, Adam Kocoloski <ko...@apache.org> wrote:

> On Nov 1, 2012, at 1:41 AM, Benoit Chesneau <bc...@gmail.com> wrote:
>
> > ok I will stop for now pushed 431_feature_cors (the removing of caps is
> > wanted here and I will be happy to just update the jira title if someone
> > really care) .
>
> Sure, I only used caps because CORS is an acronym.  It doesn't matter at
> all.  Is there a reason why you keep using underscores instead of hyphens?
>
> > Side note: Imo adding feature in the naming here doesn't give anything  .
> > Prefixing by feature/ at least would optimize queries for rapid eye
> looking
> > or a bot going over the issues...
>
> Probably best to bring that up as an objection to Jan's proposal in the
> "branching in couchdb" thread.  Personally I prefer the 431-feature-cors
> style from that proposal.
>

I'm doing it now. The point of using feature/ and fix/ is to help later
when we will all works actively on different features/ fixes/ and the
number of branch will be larger. Using XXX-feature_... will not help at all
in this case. I know this isn't a problem today but I prefer having a
strict naming like this at first rather than trying to change things later.
Especially since this is a known problem.



> I realize I'm being incredibly pedantic, but it just rubbed me the wrong
> way that we collectively wrote 9 emails on the topic branch naming today,
> got what appeared to be lazy consensus on a convention, and then almost
> immediately ignored that convention.  Regards,
>

That wasn't intended. I may have missed that we reached a consensus on a
new naming. For hyphen and such well... I will fix it again, though i think
using - helps to separate meanings when _ is normally used to extend a
meaning of the same info (here ticket number vs short description) .

- benoît

Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Adam Kocoloski <ko...@apache.org>.
On Nov 1, 2012, at 1:41 AM, Benoit Chesneau <bc...@gmail.com> wrote:

> ok I will stop for now pushed 431_feature_cors (the removing of caps is
> wanted here and I will be happy to just update the jira title if someone
> really care) .

Sure, I only used caps because CORS is an acronym.  It doesn't matter at all.  Is there a reason why you keep using underscores instead of hyphens?

> Side note: Imo adding feature in the naming here doesn't give anything  .
> Prefixing by feature/ at least would optimize queries for rapid eye looking
> or a bot going over the issues...

Probably best to bring that up as an objection to Jan's proposal in the "branching in couchdb" thread.  Personally I prefer the 431-feature-cors style from that proposal.

I realize I'm being incredibly pedantic, but it just rubbed me the wrong way that we collectively wrote 9 emails on the topic branch naming today, got what appeared to be lazy consensus on a convention, and then almost immediately ignored that convention.  Regards,

Adam

Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Benoit Chesneau <bc...@gmail.com>.
ok I will stop for now pushed 431_feature_cors (the removing of caps is
wanted here and I will be happy to just update the jira title if someone
really care) .

Side note: Imo adding feature in the naming here doesn't give anything  .
Prefixing by feature/ at least would optimize queries for rapid eye looking
or a bot going over the issues...


On Thu, Nov 1, 2012 at 6:36 AM, Benoit Chesneau <bc...@gmail.com> wrote:

> that's naming is really awkward should be feature/431-cors  (why keeping
> caps?) . Anyway changing it again.
>
>
> On Thu, Nov 1, 2012 at 6:33 AM, Adam Kocoloski <ko...@apache.org>wrote:
>
>> Benoit, sorry to keep nagging, but one of the specific conclusions of the
>> thread on branch naming that you started earlier today was that the branch
>> name should indicate whether it's a *feature* or a *bugfix*.  In this case
>> the syntax would be 431-feature-CORS.
>>
>> Adam
>>
>> On Nov 1, 2012, at 1:30 AM, Benoit Chesneau <bc...@gmail.com> wrote:
>>
>> > The branch have been renamed to 431_cors. I'm not sure it will be
>> renamed
>> > on github too.
>> >
>> > - benoit
>> >
>> >
>> > On Thu, Nov 1, 2012 at 5:34 AM, Paul Davis <paul.joseph.davis@gmail.com
>> >wrote:
>> >
>> >> On Wed, Oct 31, 2012 at 8:14 PM, Adam Kocoloski <ko...@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 <bc...@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 <bc...@gmail.com>
>> >>>>>> Authored: Thu Nov 1 00:41:00 2012 +0100
>> >>>>>> Committer: benoitc <bc...@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.
>> >>>>>>
>> >>>>>
>> >>>>>
>> >>>
>> >>
>>
>>
>

Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Benoit Chesneau <bc...@gmail.com>.
that's naming is really awkward should be feature/431-cors  (why keeping
caps?) . Anyway changing it again.


On Thu, Nov 1, 2012 at 6:33 AM, Adam Kocoloski <ko...@apache.org> wrote:

> Benoit, sorry to keep nagging, but one of the specific conclusions of the
> thread on branch naming that you started earlier today was that the branch
> name should indicate whether it's a *feature* or a *bugfix*.  In this case
> the syntax would be 431-feature-CORS.
>
> Adam
>
> On Nov 1, 2012, at 1:30 AM, Benoit Chesneau <bc...@gmail.com> wrote:
>
> > The branch have been renamed to 431_cors. I'm not sure it will be renamed
> > on github too.
> >
> > - benoit
> >
> >
> > On Thu, Nov 1, 2012 at 5:34 AM, Paul Davis <paul.joseph.davis@gmail.com
> >wrote:
> >
> >> On Wed, Oct 31, 2012 at 8:14 PM, Adam Kocoloski <ko...@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 <bc...@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 <ko...@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 <bc...@gmail.com>
> >>>>>> Authored: Thu Nov 1 00:41:00 2012 +0100
> >>>>>> Committer: benoitc <bc...@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.
> >>>>>>
> >>>>>
> >>>>>
> >>>
> >>
>
>

Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Adam Kocoloski <ko...@apache.org>.
Benoit, sorry to keep nagging, but one of the specific conclusions of the thread on branch naming that you started earlier today was that the branch name should indicate whether it's a *feature* or a *bugfix*.  In this case the syntax would be 431-feature-CORS.

Adam

On Nov 1, 2012, at 1:30 AM, Benoit Chesneau <bc...@gmail.com> wrote:

> The branch have been renamed to 431_cors. I'm not sure it will be renamed
> on github too.
> 
> - benoit
> 
> 
> On Thu, Nov 1, 2012 at 5:34 AM, Paul Davis <pa...@gmail.com>wrote:
> 
>> On Wed, Oct 31, 2012 at 8:14 PM, Adam Kocoloski <ko...@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 <bc...@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 <ko...@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 <bc...@gmail.com>
>>>>>> Authored: Thu Nov 1 00:41:00 2012 +0100
>>>>>> Committer: benoitc <bc...@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.
>>>>>> 
>>>>> 
>>>>> 
>>> 
>> 


Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Benoit Chesneau <bc...@gmail.com>.
The branch have been renamed to 431_cors. I'm not sure it will be renamed
on github too.

- benoit


On Thu, Nov 1, 2012 at 5:34 AM, Paul Davis <pa...@gmail.com>wrote:

> On Wed, Oct 31, 2012 at 8:14 PM, Adam Kocoloski <ko...@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 <bc...@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 <ko...@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 <bc...@gmail.com>
> >>>> Authored: Thu Nov 1 00:41:00 2012 +0100
> >>>> Committer: benoitc <bc...@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.
> >>>>
> >>>
> >>>
> >
>

Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Paul Davis <pa...@gmail.com>.
On Wed, Oct 31, 2012 at 8:14 PM, Adam Kocoloski <ko...@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 <bc...@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 <ko...@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 <bc...@gmail.com>
>>>> Authored: Thu Nov 1 00:41:00 2012 +0100
>>>> Committer: benoitc <bc...@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.
>>>>
>>>
>>>
>

Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Adam Kocoloski <ko...@apache.org>.
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,

Adam

On Oct 31, 2012, at 7:53 PM, Benoit Chesneau <bc...@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 <ko...@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 <bc...@gmail.com>
>>> Authored: Thu Nov 1 00:41:00 2012 +0100
>>> Committer: benoitc <bc...@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.
>>> 
>> 
>> 


Re: git commit: handle CORS. fix #COUCHDB-431

Posted by Benoit Chesneau <bc...@gmail.com>.
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 <ko...@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 <bc...@gmail.com>
> > Authored: Thu Nov 1 00:41:00 2012 +0100
> > Committer: benoitc <bc...@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.
> >
>
>