couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From beno...@apache.org
Subject git commit: handle CORS. fix #COUCHDB-431
Date Thu, 01 Nov 2012 05:34:49 GMT
Updated Branches:
  refs/heads/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/431_cors
Commit: 0777262fa291a79555ea23f2ff203d1ae7654547
Parents: 88c52b2
Author: benoitc <bchesneau@gmail.com>
Authored: Thu Nov 1 00:41:00 2012 +0100
Committer: benoitc <bchesneau@gmail.com>
Committed: Thu Nov 1 00:41:00 2012 +0100

----------------------------------------------------------------------
 etc/couchdb/default.ini.tpl.in    |   23 +++-
 src/couchdb/Makefile.am           |    4 +-
 src/couchdb/couch_httpd.erl       |   53 ++++++--
 src/couchdb/couch_httpd_cors.erl  |  230 ++++++++++++++++++++++++++++++++
 src/couchdb/couch_httpd_vhost.erl |   55 ++++----
 test/etap/231_cors.t              |  230 ++++++++++++++++++++++++++++++++
 6 files changed, 553 insertions(+), 42 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/etc/couchdb/default.ini.tpl.in
----------------------------------------------------------------------
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 79ece5c..6a32f65 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -49,6 +49,7 @@ allow_jsonp = false
 ; For more socket options, consult Erlang's module 'inet' man page.
 ;socket_options = [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, true}]
 log_max_chunk_size = 1000000
+cors_enable = false
 
 [ssl]
 port = 6984
@@ -67,6 +68,26 @@ auth_cache_size = 50 ; size is number of cache entries
 allow_persistent_cookies = false ; set to true to allow persistent cookies
 iterations = 10000 ; iterations for password hashing
 
+[cors]
+allows_credentials = false
+; List of origins separated by a comma
+;origins =
+; List of accepted headers separated by a comma
+; headers =
+; List of accepted methods
+; methods =
+
+
+; Configuration for a vhost
+:[cors:example.com]
+; allows_credentials = false
+; List of origins separated by a comma
+;origins =
+; List of accepted headers separated by a comma
+; headers =
+; List of accepted methods
+; methods =
+
 [couch_httpd_oauth]
 ; If set to 'true', oauth token and consumer secrets will be looked up
 ; in the authentication database (_users). These secrets are stored in
@@ -224,7 +245,7 @@ socket_options = [{keepalive, true}, {nodelay, false}]
 ;cert_file = /full/path/to/server_cert.pem
 ; Path to file containing user's private PEM encoded key.
 ;key_file = /full/path/to/server_key.pem
-; String containing the user's password. Only used if the private keyfile is password protected.

+; String containing the user's password. Only used if the private keyfile is password protected.
 ;password = somepassword
 ; Set to true to validate peer certificates.
 verify_ssl_certificates = false

http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/Makefile.am
----------------------------------------------------------------------
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am
index 5705976..9fe19bc 100644
--- a/src/couchdb/Makefile.am
+++ b/src/couchdb/Makefile.am
@@ -49,6 +49,7 @@ source_files = \
     couch_httpd.erl \
     couch_httpd_db.erl \
     couch_httpd_auth.erl \
+    couch_httpd_cors.erl \
     couch_httpd_oauth.erl \
     couch_httpd_external.erl \
     couch_httpd_misc_handlers.erl \
@@ -79,7 +80,7 @@ source_files = \
     couch_work_queue.erl \
     json_stream_parse.erl
 
-EXTRA_DIST = $(source_files) couch_db.hrl couch_js_functions.hrl 
+EXTRA_DIST = $(source_files) couch_db.hrl couch_js_functions.hrl
 
 compiled_files = \
     couch.app \
@@ -106,6 +107,7 @@ compiled_files = \
     couch_httpd_db.beam \
     couch_httpd_auth.beam \
     couch_httpd_oauth.beam \
+    couch_httpd_cors.beam \
     couch_httpd_proxy.beam \
     couch_httpd_external.beam \
     couch_httpd_misc_handlers.beam \

http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl
index 45ceebc..6bba871 100644
--- a/src/couchdb/couch_httpd.erl
+++ b/src/couchdb/couch_httpd.erl
@@ -275,7 +275,10 @@ handle_request_int(MochiReq, DefaultFun,
 
     % allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE
header
     MethodOverride = MochiReq:get_primary_header_value("X-HTTP-Method-Override"),
-    Method2 = case lists:member(MethodOverride, ["GET", "HEAD", "POST", "PUT", "DELETE",
"TRACE", "CONNECT", "COPY"]) of
+    Method2 = case lists:member(MethodOverride, ["GET", "HEAD", "POST",
+                                                 "PUT", "DELETE",
+                                                 "TRACE", "CONNECT",
+                                                 "COPY"]) of
     true ->
         ?LOG_INFO("MethodOverride: ~s (real method was ~s)", [MethodOverride, Method1]),
         case Method1 of
@@ -312,13 +315,19 @@ handle_request_int(MochiReq, DefaultFun,
     HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun),
     {ok, AuthHandlers} = application:get_env(couch, auth_handlers),
 
+    ?LOG_INFO("fuck you ~p~n", [Method]),
     {ok, Resp} =
     try
-        case authenticate_request(HttpReq, AuthHandlers) of
-        #httpd{} = Req ->
-            HandlerFun(Req);
-        Response ->
-            Response
+        case couch_httpd_cors:is_preflight_request(HttpReq) of
+            #httpd{} ->
+                case authenticate_request(HttpReq, AuthHandlers) of
+                #httpd{} = Req ->
+                    HandlerFun(Req);
+                Response ->
+                    Response
+                end;
+            Response ->
+                Response
         end
     catch
         throw:{http_head_abort, Resp0} ->
@@ -450,10 +459,13 @@ accepted_encodings(#httpd{mochi_req=MochiReq}) ->
 serve_file(Req, RelativePath, DocumentRoot) ->
     serve_file(Req, RelativePath, DocumentRoot, []).
 
-serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot, ExtraHeaders) ->
+serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot,
+           ExtraHeaders) ->
     log_request(Req, 200),
-    {ok, MochiReq:serve_file(RelativePath, DocumentRoot,
-        server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []) ++ ExtraHeaders)}.
+    {ok, MochiReq:serve_file(RelativePath, DocumentRoot, server_header() ++
+                             couch_httpd_cors:cors_headers(Req) ++
+                             couch_httpd_auth:cookie_auth_header(Req, []) ++
+                             ExtraHeaders)}.
 
 qs_value(Req, Key) ->
     qs_value(Req, Key, undefined).
@@ -603,7 +615,10 @@ log_request(#httpd{mochi_req=MochiReq,peer=Peer}, Code) ->
 start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) ->
     log_request(Req, Code),
     couch_stats_collector:increment({httpd_status_codes, Code}),
-    Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req,
Headers), Length}),
+    Headers1 = Headers ++ server_header() ++
+               couch_httpd_auth:cookie_auth_header(Req, Headers) ++
+               couch_httpd_cors:cors_headers(Req),
+    Resp = MochiReq:start_response_length({Code, Headers1, Length}),
     case MochiReq:get(method) of
     'HEAD' -> throw({http_head_abort, Resp});
     _ -> ok
@@ -614,7 +629,8 @@ start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
     log_request(Req, Code),
     couch_stats_collector:increment({httpd_status_codes, Code}),
     CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers),
-    Headers2 = Headers ++ server_header() ++ CookieHeader,
+    Headers2 = Headers ++ server_header() ++ CookieHeader ++
+               couch_httpd_cors:cors_headers(Req),
     Resp = MochiReq:start_response({Code, Headers2}),
     case MochiReq:get(method) of
         'HEAD' -> throw({http_head_abort, Resp});
@@ -646,8 +662,11 @@ http_1_0_keep_alive(Req, Headers) ->
 start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
     log_request(Req, Code),
     couch_stats_collector:increment({httpd_status_codes, Code}),
-    Headers2 = http_1_0_keep_alive(MochiReq, Headers),
-    Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req,
Headers2), chunked}),
+    Headers1 = http_1_0_keep_alive(MochiReq, Headers),
+    Headers2 = Headers1 ++ server_header() ++
+               couch_httpd_auth:cookie_auth_header(Req, Headers1) ++
+               couch_httpd_cors:cors_headers(Req),
+    Resp = MochiReq:respond({Code, Headers2, chunked}),
     case MochiReq:get(method) of
     'HEAD' -> throw({http_head_abort, Resp});
     _ -> ok
@@ -668,14 +687,18 @@ last_chunk(Resp) ->
 send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) ->
     log_request(Req, Code),
     couch_stats_collector:increment({httpd_status_codes, Code}),
-    Headers2 = http_1_0_keep_alive(MochiReq, Headers),
+    Headers1 = http_1_0_keep_alive(MochiReq, Headers),
     if Code >= 500 ->
         ?LOG_ERROR("httpd ~p error response:~n ~s", [Code, Body]);
     Code >= 400 ->
         ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]);
     true -> ok
     end,
-    {ok, MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req,
Headers2), Body})}.
+    Headers2 = Headers1 ++ server_header() ++
+               couch_httpd_cors:cors_headers(Req) ++
+               couch_httpd_auth:cookie_auth_header(Req, Headers1),
+
+    {ok, MochiReq:respond({Code, Headers2, Body})}.
 
 send_method_not_allowed(Req, Methods) ->
     send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only
" ++ Methods ++ " allowed")).

http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd_cors.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_httpd_cors.erl b/src/couchdb/couch_httpd_cors.erl
new file mode 100644
index 0000000..69f57ed
--- /dev/null
+++ b/src/couchdb/couch_httpd_cors.erl
@@ -0,0 +1,230 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+%% @doc module to handle Cross-Origin Resource Sharing
+%%
+%% This module handles CROSS requests and preflight request for a
+%% couchdb Node. The config is done in the ini file.
+
+
+-module(couch_httpd_cors).
+
+-include("couch_db.hrl").
+
+-export([is_preflight_request/1, cors_headers/1]).
+
+-define(SUPPORTED_HEADERS, "Accept, Accept-Language, Content-Type," ++
+        "Expires, Last-Modified, Pragma, Origin, Content-Length," ++
+        "If-Match, Destination, X-Requested-With, " ++
+        "X-Http-Method-Override, Content-Range").
+
+-define(SUPPORTED_METHODS, "GET, HEAD, POST, PUT, DELETE," ++
+        "TRACE, CONNECT, COPY, OPTIONS").
+
+is_preflight_request(#httpd{method=Method}=Req) when Method /= 'OPTIONS' ->
+    Req;
+is_preflight_request(#httpd{mochi_req=MochiReq}=Req) ->
+    case get_bool_config("httpd", "enable_cors", false) of
+        true ->
+            case preflight_request(MochiReq) of
+                {ok, PreflightHeaders} ->
+                    couch_httpd:send_response(Req, 204, PreflightHeaders, <<>>);
+                _ ->
+                    Req
+            end;
+        false ->
+            Req
+    end.
+
+
+cors_headers(#httpd{mochi_req=MochiReq}) ->
+    Host = couch_httpd_vhost:host(MochiReq),
+    case get_bool_config("httpd", "enable_cors", false) of
+        true ->
+            AcceptedOrigins = re:split(cors_config(Host, "origins", []),
+                                       "\\s*,\\s*",
+                                       [trim, {return, list}]),
+            case MochiReq:get_header_value("Origin") of
+                undefined ->
+                    [];
+                <<"*">> ->
+                    handle_cors_headers("*", Host, AcceptedOrigins);
+                <<"null">> ->
+                    handle_cors_headers("*", Host, AcceptedOrigins);
+                Origin ->
+                    handle_cors_headers(couch_util:to_list(Origin),
+                                        Host, AcceptedOrigins)
+            end;
+        false ->
+            []
+    end.
+
+handle_cors_headers("*", _Host, _AcceptedOrigins) ->
+    [{"Access-Control-Allow-Origin", "*"}];
+handle_cors_headers(Origin, Host, []) ->
+    case allows_credentials(Origin, Host) of
+        true ->
+            [{"Access-Control-Allow-Origin", Origin},
+             {"Access-Control-Allow-Credentials", "true"}];
+        false ->
+            [{"Access-Control-Allow-Origin", Origin}]
+    end;
+handle_cors_headers(Origin, Host, AcceptedOrigins) ->
+    AllowsCredentials = allows_credentials(Origin, Host),
+    case lists:member(Origin, AcceptedOrigins) of
+        true when AllowsCredentials =:= true ->
+            [{"Access-Control-Allow-Origin", Origin},
+             {"Access-Control-Allow-Credentials", "true"}];
+        true ->
+            [{"Access-Control-Allow-Origin", Origin}];
+        _ ->
+            []
+    end.
+
+
+preflight_request(MochiReq) ->
+    Host = couch_httpd_vhost:host(MochiReq),
+    case MochiReq:get_header_value("Origin") of
+        undefined ->
+            MochiReq;
+        <<"*">>  ->
+            handle_preflight_request("*", Host, MochiReq);
+        <<"null">> ->
+            handle_preflight_request("*", Host, MochiReq);
+        Origin ->
+            AcceptedOrigins = re:split(cors_config(Host, "origins", []),
+                                       "\\s*,\\s*",
+                                       [trim, {return, list}]),
+            case AcceptedOrigins of
+                [] ->
+                    handle_preflight_request(couch_util:to_list(Origin),
+                                             Host, MochiReq);
+                _ ->
+                    case lists:member(Origin, AcceptedOrigins) of
+                        true ->
+                            handle_preflight_request(couch_util:to_list(Origin),
+                                                     Host, MochiReq);
+                        false ->
+                            false
+                    end
+            end
+    end.
+
+handle_preflight_request(Origin, Host, MochiReq) ->
+    %% get supported methods
+    SupportedMethods = split_list(cors_config(Host, "methods",
+                                              ?SUPPORTED_METHODS)),
+
+    % get supported headers
+    AllSupportedHeaders = split_list(cors_config(Host, "headers",
+                                                 ?SUPPORTED_HEADERS)),
+
+    SupportedHeaders = [string:to_lower(H) || H <- AllSupportedHeaders],
+
+    % get max age
+    MaxAge = cors_config(Host, "max_age", "12345"),
+
+    PreflightHeaders0 = case allows_credentials(Origin, Host) of
+        true ->
+            [{"Access-Control-Allow-Origin", Origin},
+             {"Access-Control-Allow-Credentials", "true"},
+             {"Access-Control-Max-Age", MaxAge},
+             {"Access-Control-Allow-Methods", string:join(SupportedMethods,
+                                                          ", ")}];
+        false ->
+            [{"Access-Control-Allow-Origin", Origin},
+             {"Access-Control-Max-Age", MaxAge},
+             {"Access-Control-Allow-Methods", string:join(SupportedMethods,
+                                                          ", ")}]
+    end,
+
+    case MochiReq:get_header_value("Access-Control-Request-Method") of
+        undefined ->
+            {ok, PreflightHeaders0};
+        Method ->
+            case lists:member(Method, SupportedMethods) of
+                true ->
+                    % method ok , check headers
+                    AccessHeaders = MochiReq:get_header_value(
+                            "Access-Control-Request-Headers"),
+                    {FinalReqHeaders, ReqHeaders} = case AccessHeaders of
+                        undefined -> {"", []};
+                        Headers ->
+                            % transform header list in something we
+                            % could check. make sure everything is a
+                            % list
+                            RH = [string:to_lower(H)
+                                  || H <- re:split(Headers, ",\\s*",
+                                                   [{return,list},trim])],
+                            {Headers, RH}
+                    end,
+                    % check if headers are supported
+                    case ReqHeaders -- SupportedHeaders of
+                        [] ->
+                            PreflightHeaders = PreflightHeaders0 ++
+                                               [{"Access-Control-Allow-Headers",
+                                                 FinalReqHeaders}],
+                            {ok, PreflightHeaders};
+                        _ ->
+                            false
+                    end;
+                false ->
+                    false
+            end
+    end.
+
+
+allows_credentials("*", _Host) ->
+    false;
+allows_credentials(_Origin, Host) ->
+    Default = get_bool_config("cors", "allows_credentials",
+                              false),
+
+    get_bool_config(cors_section(Host), "allows_credentials",
+                    Default).
+
+
+cors_config(Host, Key, Default) ->
+    couch_config:get(cors_section(Host), Key,
+                     couch_config:get("cors", Key, Default)).
+
+cors_section(Host0) ->
+    {Host, _Port} = split_host_port(Host0),
+    "cors:" ++ Host.
+
+get_bool_config(Section, Key, Default) ->
+    case couch_config:get(Section, Key) of
+        undefined ->
+            Default;
+        "true" ->
+            true;
+        "false" ->
+            false
+    end.
+
+split_list(S) ->
+    re:split(S, "\\s*,\\s*", [trim, {return, list}]).
+
+split_host_port(HostAsString) ->
+    case string:rchr(HostAsString, $:) of
+        0 ->
+            {HostAsString, '*'};
+        N ->
+            HostPart = string:substr(HostAsString, 1, N-1),
+            case (catch erlang:list_to_integer(string:substr(HostAsString,
+                            N+1, length(HostAsString)))) of
+                {'EXIT', _} ->
+                    {HostAsString, '*'};
+                Port ->
+                    {HostPart, Port}
+            end
+    end.

http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd_vhost.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_httpd_vhost.erl b/src/couchdb/couch_httpd_vhost.erl
index 59f05ce..4c3ebfe 100644
--- a/src/couchdb/couch_httpd_vhost.erl
+++ b/src/couchdb/couch_httpd_vhost.erl
@@ -15,7 +15,7 @@
 
 -export([start_link/0, config_change/2, reload/0, get_state/0, dispatch_host/1]).
 -export([urlsplit_netloc/2, redirect_to_vhost/2]).
-
+-export([host/1, split_host_port/1]).
 
 -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
 
@@ -32,7 +32,7 @@
 %% doc the vhost manager.
 %% This gen_server keep state of vhosts added to the ini and try to
 %% match the Host header (or forwarded) against rules built against
-%% vhost list. 
+%% vhost list.
 %%
 %% Declaration of vhosts take place in the configuration file :
 %%
@@ -51,7 +51,7 @@
 %%      "*.db.example.com = /"  will match all cname on top of db
 %% examples to the root of the machine.
 %%
-%% 
+%%
 %% Rewriting Hosts to path
 %% -----------------------
 %%
@@ -75,7 +75,7 @@
 %%    redirect_vhost_handler = {Module, Fun}
 %%
 %% The function take 2 args : the mochiweb request object and the target
-%%% path. 
+%%% path.
 
 start_link() ->
     gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@@ -98,15 +98,7 @@ dispatch_host(MochiReq) ->
     {"/" ++ VPath, Query, Fragment} = mochiweb_util:urlsplit_path(MochiReq:get(raw_path)),
     VPathParts =  string:tokens(VPath, "/"),
 
-    XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"),
-    VHost = case MochiReq:get_header_value(XHost) of
-        undefined ->
-            case MochiReq:get_header_value("Host") of
-                undefined -> [];
-                Value1 -> Value1
-            end;
-        Value -> Value
-    end,
+    VHost = host(MochiReq),
     {VHostParts, VhostPort} = split_host_port(VHost),
     FinalMochiReq = case try_bind_vhost(VHosts, lists:reverse(VHostParts),
             VhostPort, VPathParts) of
@@ -133,14 +125,14 @@ append_path("/"=_Target, "/"=_Path) ->
 append_path(Target, Path) ->
     Target ++ Path.
 
-% default redirect vhost handler 
+% default redirect vhost handler
 redirect_to_vhost(MochiReq, VhostTarget) ->
     Path = MochiReq:get(raw_path),
     Target = append_path(VhostTarget, Path),
 
     ?LOG_DEBUG("Vhost Target: '~p'~n", [Target]),
 
-    Headers = mochiweb_headers:enter("x-couchdb-vhost-path", Path, 
+    Headers = mochiweb_headers:enter("x-couchdb-vhost-path", Path,
         MochiReq:get(headers)),
 
     % build a new mochiweb request
@@ -154,7 +146,7 @@ redirect_to_vhost(MochiReq, VhostTarget) ->
     MochiReq1.
 
 %% if so, then it will not be rewritten, but will run as a normal couchdb request.
-%* normally you'd use this for _uuids _utils and a few of the others you want to 
+%* normally you'd use this for _uuids _utils and a few of the others you want to
 %% keep available on vhosts. You can also use it to make databases 'global'.
 vhost_global( VhostGlobals, MochiReq) ->
     RawUri = MochiReq:get(raw_path),
@@ -175,14 +167,14 @@ try_bind_vhost([], _HostParts, _Port, _PathParts) ->
 try_bind_vhost([VhostSpec|Rest], HostParts, Port, PathParts) ->
     {{VHostParts, VPort, VPath}, Path} = VhostSpec,
     case bind_port(VPort, Port) of
-        ok -> 
+        ok ->
             case bind_vhost(lists:reverse(VHostParts), HostParts, []) of
                 {ok, Bindings, Remainings} ->
                     case bind_path(VPath, PathParts) of
                         {ok, PathParts1} ->
                             Path1 = make_target(Path, Bindings, Remainings, []),
                             {make_path(Path1), make_path(PathParts1)};
-                        fail -> 
+                        fail ->
                             try_bind_vhost(Rest, HostParts, Port,
                                 PathParts)
                     end;
@@ -193,7 +185,7 @@ try_bind_vhost([VhostSpec|Rest], HostParts, Port, PathParts) ->
 
 %% doc: build new patch from bindings. bindings are query args
 %% (+ dynamic query rewritten if needed) and bindings found in
-%% bind_path step. 
+%% bind_path step.
 %% TODO: merge code with rewrite. But we need to make sure we are
 %% in string here.
 make_target([], _Bindings, _Remaining, Acc) ->
@@ -223,7 +215,7 @@ bind_vhost([],[], Bindings) -> {ok, Bindings, []};
 bind_vhost([?MATCH_ALL], [], _Bindings) -> fail;
 bind_vhost([?MATCH_ALL], Rest, Bindings) -> {ok, Bindings, Rest};
 bind_vhost([], _HostParts, _Bindings) -> fail;
-bind_vhost([{bind, Token}|Rest], [Match|RestHost], Bindings) -> 
+bind_vhost([{bind, Token}|Rest], [Match|RestHost], Bindings) ->
     bind_vhost(Rest, RestHost, [{{bind, Token}, Match}|Bindings]);
 bind_vhost([Cname|Rest], [Cname|RestHost], Bindings) ->
     bind_vhost(Rest, RestHost, Bindings);
@@ -243,6 +235,19 @@ bind_path(_, _) ->
 
 
 %% create vhost list from ini
+
+host(MochiReq) ->
+    XHost = couch_config:get("httpd", "x_forwarded_host",
+                             "X-Forwarded-Host"),
+    case MochiReq:get_header_value(XHost) of
+        undefined ->
+            case MochiReq:get_header_value("Host") of
+                undefined -> [];
+                Value1 -> Value1
+            end;
+        Value -> Value
+    end.
+
 make_vhosts() ->
     Vhosts = lists:foldl(fun
                 ({_, ""}, Acc) ->
@@ -267,15 +272,15 @@ parse_vhost(Vhost) ->
             H1 = make_spec(H, []),
             {H1, P, string:tokens(Path, "/")}
     end.
-            
+
 
 split_host_port(HostAsString) ->
     case string:rchr(HostAsString, $:) of
         0 ->
             {split_host(HostAsString), '*'};
         N ->
-            HostPart = string:substr(HostAsString, 1, N-1), 
-            case (catch erlang:list_to_integer(string:substr(HostAsString, 
+            HostPart = string:substr(HostAsString, 1, N-1),
+            case (catch erlang:list_to_integer(string:substr(HostAsString,
                             N+1, length(HostAsString)))) of
                 {'EXIT', _} ->
                     {split_host(HostAsString), '*'};
@@ -303,7 +308,7 @@ make_spec([P|R], Acc) ->
 
 
 parse_var(P) ->
-    case P of 
+    case P of
         ":" ++ Var ->
             {bind, Var};
         _ -> P
@@ -323,7 +328,7 @@ make_path(Parts) ->
 
 init(_) ->
     ok = couch_config:register(fun ?MODULE:config_change/2),
-    
+
     %% load configuration
     {VHostGlobals, VHosts, Fun} = load_conf(),
     State = #vhosts_state{

http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/test/etap/231_cors.t
----------------------------------------------------------------------
diff --git a/test/etap/231_cors.t b/test/etap/231_cors.t
new file mode 100644
index 0000000..72fc3df
--- /dev/null
+++ b/test/etap/231_cors.t
@@ -0,0 +1,230 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-record(user_ctx, {
+    name = null,
+    roles = [],
+    handler
+}).
+
+
+-define(SUPPORTED_METHODS, "GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, COPY, OPTIONS").
+server() ->
+    lists:concat([
+        "http://127.0.0.1:",
+        mochiweb_socket_server:get(couch_httpd, port),
+        "/"
+    ]).
+
+
+main(_) ->
+    test_util:init_code_path(),
+
+    etap:plan(11),
+    case (catch test()) of
+        ok ->
+            etap:end_tests();
+        Other ->
+            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+            etap:bail(Other)
+    end,
+    ok.
+
+dbname() -> "etap-test-db".
+dbname1() -> "etap-test-db1".
+dbname2() -> "etap-test-db2".
+
+admin_user_ctx() -> {user_ctx, #user_ctx{roles=[<<"_admin">>]}}.
+
+set_admin_password(UserName, Password) ->
+    Salt = binary_to_list(couch_uuids:random()),
+    Hashed = couch_util:to_hex(crypto:sha(Password ++ Salt)),
+    couch_config:set("admins", UserName,
+        "-hashed-" ++ Hashed ++ "," ++ Salt, false).
+
+test() ->
+
+    ibrowse:start(),
+    crypto:start(),
+
+    %% launch couchdb
+    couch_server_sup:start_link(test_util:config_files()),
+
+    %% initialize db
+    timer:sleep(1000),
+    couch_server:delete(list_to_binary(dbname()), [admin_user_ctx()]),
+    couch_server:delete(list_to_binary(dbname1()), [admin_user_ctx()]),
+    couch_server:delete(list_to_binary(dbname2()), [admin_user_ctx()]),
+    {ok, Db} = couch_db:create(list_to_binary(dbname()), [admin_user_ctx()]),
+    {ok, Db1} = couch_db:create(list_to_binary(dbname1()), [admin_user_ctx()]),
+    {ok, Db2} = couch_db:create(list_to_binary(dbname2()), [admin_user_ctx()]),
+
+    % CORS is disabled by default
+    test_no_headers_server(),
+    test_no_headers_db(),
+
+    % Now enable CORS
+    ok = couch_config:set("httpd", "enable_cors", "true", false),
+    ok = couch_config:set("cors", "origins", "http://example.com", false),
+
+    %% do tests
+    test_incorrect_origin_simple_request(),
+    test_incorrect_origin_preflight_request(),
+
+    test_preflight_request(),
+    test_db_request(),
+    test_db_preflight_request(),
+    test_db_origin_request(),
+    test_db1_origin_request(),
+
+    %% do tests with auth
+    ok = set_admin_password("test", "test"),
+
+    test_db_preflight_auth_request(),
+    test_db_origin_auth_request(),
+
+    %% restart boilerplate
+    catch couch_db:close(Db),
+    catch couch_db:close(Db1),
+    catch couch_db:close(Db2),
+
+    couch_server:delete(list_to_binary(dbname()), [admin_user_ctx()]),
+    couch_server:delete(list_to_binary(dbname1()), [admin_user_ctx()]),
+    couch_server:delete(list_to_binary(dbname2()), [admin_user_ctx()]),
+
+    timer:sleep(3000),
+    couch_server_sup:stop(),
+    ok.
+
+%% Cors is disabled, should not return Access-Control-Allow-Origin
+test_no_headers_server() ->
+    Headers = [{"Origin", "http://127.0.0.1"}],
+    {ok, _, Resp, _} = ibrowse:send_req(server(), Headers, get, []),
+    etap:is(proplists:get_value("Access-Control-Allow-Origin", Resp),
+            undefined, "No CORS Headers when disabled").
+
+%% Cors is disabled, should not return Access-Control-Allow-Origin
+test_no_headers_db() ->
+    Headers = [{"Origin", "http://127.0.0.1"}],
+    Url = server() ++ "etap-test-db",
+    {ok, _, Resp, _} = ibrowse:send_req(Url, Headers, get, []),
+    etap:is(proplists:get_value("Access-Control-Allow-Origin", Resp),
+            undefined, "No CORS Headers when disabled").
+
+test_incorrect_origin_simple_request() ->
+    Headers = [{"Origin", "http://127.0.0.1"}],
+    {ok, _, RespHeaders, _} = ibrowse:send_req(server(), Headers, get, []),
+    etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders),
+            undefined,
+            "Specified invalid origin, no Access").
+
+test_incorrect_origin_preflight_request() ->
+    Headers = [{"Origin", "http://127.0.0.1"},
+               {"Access-Control-Request-Method", "GET"}],
+    {ok, _, RespHeaders, _} = ibrowse:send_req(server(), Headers, options, []),
+    etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders),
+            undefined,
+            "invalid origin").
+
+test_preflight_request() ->
+    Headers = [{"Origin", "http://example.com"},
+               {"Access-Control-Request-Method", "GET"}],
+    case ibrowse:send_req(server(), Headers, options, []) of
+    {ok, _, RespHeaders, _}  ->
+        etap:is(proplists:get_value("Access-Control-Allow-Methods", RespHeaders),
+            ?SUPPORTED_METHODS,
+            "test_preflight_request Access-Control-Allow-Methods ok");
+    _ ->
+        etap:is(false, true, "ibrowse failed")
+    end.
+
+test_db_request() ->
+    Headers = [{"Origin", "http://example.com"}],
+    Url = server() ++ "etap-test-db",
+    case ibrowse:send_req(Url, Headers, get, []) of
+    {ok, _, RespHeaders, _Body} ->
+        etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders),
+            "http://example.com",
+            "db Access-Control-Allow-Origin ok");
+    _ ->
+        etap:is(false, true, "ibrowse failed")
+    end.
+
+test_db_preflight_request() ->
+    Url = server() ++ "etap-test-db",
+    Headers = [{"Origin", "http://example.com"},
+               {"Access-Control-Request-Method", "GET"}],
+    case ibrowse:send_req(Url, Headers, options, []) of
+    {ok, _, RespHeaders, _} ->
+        etap:is(proplists:get_value("Access-Control-Allow-Methods", RespHeaders),
+                ?SUPPORTED_METHODS,
+                "db Access-Control-Allow-Methods ok");
+    _ ->
+        etap:is(false, true, "ibrowse failed")
+    end.
+
+
+test_db_origin_request() ->
+    Headers = [{"Origin", "http://example.com"}],
+    Url = server() ++ "etap-test-db",
+    case ibrowse:send_req(Url, Headers, get, []) of
+    {ok, _, RespHeaders, _Body} ->
+        etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders),
+            "http://example.com",
+            "db origin ok");
+    _ ->
+        etap:is(false, true, "ibrowse failed")
+    end.
+
+test_db1_origin_request() ->
+    Headers = [{"Origin", "http://example.com"}],
+    Url = server() ++ "etap-test-db1",
+    case ibrowse:send_req(Url, Headers, get, [], [{host_header, "example.com"}]) of
+    {ok, _, RespHeaders, _Body} ->
+        etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders),
+            "http://example.com",
+            "db origin ok");
+    _Else ->
+        io:format("else ~p~n", [_Else]),
+        etap:is(false, true, "ibrowse failed")
+    end.
+
+test_db_preflight_auth_request() ->
+    Url = server() ++ "etap-test-db2",
+    Headers = [{"Origin", "http://example.com"},
+               {"Access-Control-Request-Method", "GET"}],
+    case ibrowse:send_req(Url, Headers, options, []) of
+    {ok, _Status, RespHeaders, _} ->
+        etap:is(proplists:get_value("Access-Control-Allow-Methods", RespHeaders),
+                ?SUPPORTED_METHODS,
+                "db Access-Control-Allow-Methods ok");
+    _ ->
+        etap:is(false, true, "ibrowse failed")
+    end.
+
+
+test_db_origin_auth_request() ->
+    Headers = [{"Origin", "http://example.com"}],
+    Url = server() ++ "etap-test-db2",
+
+    case ibrowse:send_req(Url, Headers, get, [],
+        [{basic_auth, {"test", "test"}}]) of
+    {ok, _, RespHeaders, _Body} ->
+        etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders),
+            "http://example.com",
+            "db origin ok");
+    _ ->
+        etap:is(false, true, "ibrowse failed")
+    end.


Mime
View raw message