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.
> >
>
>