couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From chewbra...@apache.org
Subject [1/2] chttpd commit: updated refs/heads/2080-port-cors to c93846a
Date Thu, 06 Nov 2014 22:55:22 GMT
Repository: couchdb-chttpd
Updated Branches:
  refs/heads/2080-port-cors b420d5055 -> c93846a7e (forced update)


CORS implementation for chttpd


Project: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/commit/7c9fbe65
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/tree/7c9fbe65
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/diff/7c9fbe65

Branch: refs/heads/2080-port-cors
Commit: 7c9fbe650b068fcd92709d7dc04256f8794ace78
Parents: b44515f
Author: Russell Branca <chewbranca@apache.org>
Authored: Tue Nov 4 16:07:38 2014 -0800
Committer: Russell Branca <chewbranca@apache.org>
Committed: Thu Nov 6 14:43:48 2014 -0800

----------------------------------------------------------------------
 include/chttpd_cors.hrl   |  81 +++++++
 src/chttpd.erl            |   4 +-
 src/chttpd_cors.erl       | 335 +++++++++++++++++++++++++-
 test/chttpd_cors_test.erl | 519 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 932 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/7c9fbe65/include/chttpd_cors.hrl
----------------------------------------------------------------------
diff --git a/include/chttpd_cors.hrl b/include/chttpd_cors.hrl
new file mode 100644
index 0000000..1988d7b
--- /dev/null
+++ b/include/chttpd_cors.hrl
@@ -0,0 +1,81 @@
+% 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.
+
+
+-define(SUPPORTED_HEADERS, [
+    "accept",
+    "accept-language",
+    "authorization",
+    "content-length",
+    "content-range",
+    "content-type",
+    "destination",
+    "expires",
+    "if-match",
+    "last-modified",
+    "origin",
+    "pragma",
+    "x-couch-full-commit",
+    "x-couch-id",
+    "x-couch-persist",
+    "x-couchdb-www-authenticate",
+    "x-http-method-override",
+    "x-requested-with",
+    "x-couchdb-vhost-path"
+]).
+
+
+-define(SUPPORTED_METHODS, [
+    "CONNECT",
+    "COPY",
+    "DELETE",
+    "GET",
+    "HEAD",
+    "OPTIONS",
+    "POST",
+    "PUT",
+    "TRACE"
+]).
+
+
+%% as defined in http://www.w3.org/TR/cors/#terminology
+-define(SIMPLE_HEADERS, [
+    "cache-control",
+    "content-language",
+    "content-type",
+    "expires",
+    "last-modified",
+    "pragma"
+]).
+
+
+-define(COUCH_HEADERS, [
+    "accept-ranges",
+    "etag",
+    "server",
+    "x-couch-request-id",
+    "x-couch-update-newrev",
+    "x-couchdb-body-time"
+]).
+
+
+-define(SIMPLE_CONTENT_TYPE_VALUES, [
+    "application/x-www-form-urlencoded",
+    "multipart/form-data",
+    "text/plain"
+]).
+
+
+-define(CORS_DEFAULT_MAX_AGE, 600).
+
+
+-define(CORS_DEFAULT_ALLOW_CREDENTIALS, false).

http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/7c9fbe65/src/chttpd.erl
----------------------------------------------------------------------
diff --git a/src/chttpd.erl b/src/chttpd.erl
index 8945d0a..50fcd04 100644
--- a/src/chttpd.erl
+++ b/src/chttpd.erl
@@ -200,8 +200,8 @@ handle_request(MochiReq) ->
     Result =
     try
         check_request_uri_length(RawUri),
-        case chttpd_cors:is_preflight_request(HttpReq) of
-        #httpd{} ->
+        case chttpd_cors:maybe_handle_preflight_request(HttpReq) of
+        not_preflight ->
             case authenticate_request(HttpReq, AuthenticationFuns) of
             #httpd{} = Req ->
                 HandlerFun = url_handler(HandlerKey),

http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/7c9fbe65/src/chttpd_cors.erl
----------------------------------------------------------------------
diff --git a/src/chttpd_cors.erl b/src/chttpd_cors.erl
index 03ec289..65709f2 100644
--- a/src/chttpd_cors.erl
+++ b/src/chttpd_cors.erl
@@ -12,10 +12,335 @@
 
 -module(chttpd_cors).
 
--export([is_preflight_request/1, headers/2]).
 
-is_preflight_request(Req) ->
-    couch_httpd_cors:is_preflight_request(Req).
+-export([
+    maybe_handle_preflight_request/1,
+    maybe_handle_preflight_request/2,
+    headers/2,
+    headers/4
+]).
+-export([
+    is_cors_enabled/1,
+    get_cors_config/1
+]).
 
-headers(Req, Headers) ->
-    couch_httpd_cors:cors_headers(Req, Headers).
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("chttpd/include/chttpd_cors.hrl").
+
+
+%% http://www.w3.org/TR/cors/#resource-preflight-requests
+
+maybe_handle_preflight_request(#httpd{method=Method}) when Method /= 'OPTIONS' ->
+    not_preflight;
+maybe_handle_preflight_request(Req) ->
+    maybe_handle_preflight_request(Req, get_cors_config(Req)).
+
+
+maybe_handle_preflight_request(#httpd{}=Req, Config) ->
+    case is_cors_enabled(Config) of
+        true ->
+            case preflight_request(Req, Config) of
+                {ok, PreflightHeaders} ->
+                    chttpd:send_response(Req, 204, PreflightHeaders, <<>>);
+                not_preflight ->
+                    not_preflight;
+                UnknownError ->
+                    couch_log:error(
+                        "Unknown response of chttpd_cors:preflight_request(~p): ~p",
+                        [Req, UnknownError]
+                    ),
+                    not_preflight
+            end;
+        false ->
+            not_preflight
+    end.
+
+
+preflight_request(Req, Config) ->
+    case get_origin(Req) of
+        undefined ->
+            %% If the Origin header is not present terminate this set of
+            %% steps. The request is outside the scope of this specification.
+            %% http://www.w3.org/TR/cors/#resource-preflight-requests
+            not_preflight;
+        Origin ->
+            AcceptedOrigins = get_accepted_origins(Req, Config),
+            AcceptAll = lists:member(<<"*">>, AcceptedOrigins),
+
+            HandlerFun = fun() ->
+                handle_preflight_request(Req, Config, Origin)
+            end,
+
+            %% We either need to accept all origins or have it listed
+            %% in our origins. Origin can only contain a single origin
+            %% as the user agent will not follow redirects [1]. If the
+            %% value of the Origin header is not a case-sensitive
+            %% match for any of the values in list of origins do not
+            %% set any additional headers and terminate this set
+            %% of steps [1].
+            %%
+            %% [1]: http://www.w3.org/TR/cors/#resource-preflight-requests
+            %%
+            %% TODO: Square against multi origin Security Considerations and the
+            %% Vary header
+            %%
+            case AcceptAll orelse lists:member(Origin, AcceptedOrigins) of
+                true -> HandlerFun();
+                false -> not_preflight
+            end
+    end.
+
+
+handle_preflight_request(Req, Config, Origin) ->
+    case chttpd:header_value(Req, "Access-Control-Request-Method") of
+    undefined ->
+        %% If there is no Access-Control-Request-Method header
+        %% or if parsing failed, do not set any additional headers
+        %% and terminate this set of steps. The request is outside
+        %% the scope of this specification.
+        %% http://www.w3.org/TR/cors/#resource-preflight-requests
+        not_preflight;
+    Method ->
+        SupportedMethods = get_origin_config(Config, Origin,
+                <<"allow_methods">>, ?SUPPORTED_METHODS),
+
+        %% get max age
+        MaxAge = couch_util:get_value("max_age", Config, ?CORS_DEFAULT_MAX_AGE),
+
+        PreflightHeaders0 = maybe_add_credentials(Config, Origin, [
+            {"Access-Control-Allow-Origin", Origin},
+            {"Access-Control-Max-Age", MaxAge},
+            {"Access-Control-Allow-Methods",
+                string:join(SupportedMethods, ", ")}]),
+
+        case lists:member(Method, SupportedMethods) of
+            true ->
+                %% method ok , check headers
+                AccessHeaders = chttpd:header_value(Req,
+                    "Access-Control-Request-Headers"),
+                {FinalReqHeaders, ReqHeaders} = case AccessHeaders of
+                    undefined -> {"", []};
+                    Headers ->
+                        %% transform header list in something we
+                        %% could check. make sure everything is a
+                        %% list
+                        RH = [string:to_lower(H)
+                              || H <- split_headers(Headers)],
+                        {Headers, RH}
+                end,
+                %% check if headers are supported
+                case ReqHeaders -- ?SUPPORTED_HEADERS of
+                [] ->
+                    PreflightHeaders = PreflightHeaders0 ++
+                                       [{"Access-Control-Allow-Headers",
+                                         FinalReqHeaders}],
+                    {ok, PreflightHeaders};
+                _ ->
+                    not_preflight
+                end;
+            false ->
+            %% If method is not a case-sensitive match for any of
+            %% the values in list of methods do not set any additional
+            %% headers and terminate this set of steps.
+            %% http://www.w3.org/TR/cors/#resource-preflight-requests
+                not_preflight
+        end
+    end.
+
+
+headers(Req, RequestHeaders) ->
+    case get_origin(Req) of
+        undefined ->
+            %% If the Origin header is not present terminate
+            %% this set of steps. The request is outside the scope
+            %% of this specification.
+            %% http://www.w3.org/TR/cors/#resource-processing-model
+            RequestHeaders;
+        Origin ->
+            headers(Req, RequestHeaders, Origin, get_cors_config(Req))
+    end.
+
+
+headers(_Req, RequestHeaders, undefined, _Config) ->
+    RequestHeaders;
+headers(Req, RequestHeaders, Origin, Config) when is_list(Origin) ->
+    headers(Req, RequestHeaders, ?l2b(string:to_lower(Origin)), Config);
+headers(Req, RequestHeaders, Origin, Config) ->
+    case is_cors_enabled(Config) of
+        true ->
+            AcceptedOrigins = get_accepted_origins(Req, Config),
+            CorsHeaders = handle_headers(Config, Origin, AcceptedOrigins),
+            maybe_apply_headers(CorsHeaders, RequestHeaders);
+        false ->
+            RequestHeaders
+    end.
+
+
+maybe_apply_headers([], RequestHeaders) ->
+    RequestHeaders;
+maybe_apply_headers(CorsHeaders, RequestHeaders) ->
+    %% Find all non ?SIMPLE_HEADERS and and non ?SIMPLE_CONTENT_TYPE_VALUES,
+    %% expose those through Access-Control-Expose-Headers, allowing
+    %% the client to access them in the browser. Also append in
+    %% ?COUCH_HEADERS, as further headers may be added later that
+    %% need to be exposed.
+    %% return: RequestHeaders ++ CorsHeaders ++ ACEH
+
+    ExposedHeaders0 = simple_headers([K || {K,_V} <- RequestHeaders]),
+
+    %% If Content-Type is not in ExposedHeaders, and the Content-Type
+    %% is not a member of ?SIMPLE_CONTENT_TYPE_VALUES, then add it
+    %% into the list of ExposedHeaders
+    ContentType =  proplists:get_value("content-type", ExposedHeaders0),
+    IncludeContentType = case ContentType of
+        undefined ->
+            false;
+        _ ->
+            lists:member(string:to_lower(ContentType), ?SIMPLE_CONTENT_TYPE_VALUES)
+        end,
+    ExposedHeaders = case IncludeContentType of
+        false ->
+            ["content-type" | lists:delete("content-type", ExposedHeaders0)];
+        true ->
+            ExposedHeaders0
+        end,
+    %% ?COUCH_HEADERS may get added later, so expose them by default
+    ACEH = [{"Access-Control-Expose-Headers",
+        string:join(ExposedHeaders ++ ?COUCH_HEADERS, ", ")}],
+    CorsHeaders ++ RequestHeaders ++ ACEH.
+
+
+simple_headers(Headers) ->
+    LCHeaders = [string:to_lower(H) || H <- Headers],
+    lists:filter(fun(H) -> lists:member(H, ?SIMPLE_HEADERS) end, LCHeaders).
+
+
+handle_headers(_Config, _Origin, []) ->
+    [];
+handle_headers(Config, Origin, AcceptedOrigins) ->
+    AcceptAll = lists:member(<<"*">>, AcceptedOrigins),
+    case AcceptAll orelse lists:member(Origin, AcceptedOrigins) of
+    true ->
+        make_cors_header(Config, Origin);
+    false ->
+        %% If the value of the Origin header is not a
+        %% case-sensitive match for any of the values
+        %% in list of origins, do not set any additional
+        %% headers and terminate this set of steps.
+        %% http://www.w3.org/TR/cors/#resource-requests
+        []
+    end.
+
+
+make_cors_header(Config, Origin) ->
+    Headers = [{"Access-Control-Allow-Origin", ?b2l(Origin)}],
+    maybe_add_credentials(Config, Origin, Headers).
+
+
+%% util
+
+
+maybe_add_credentials(Config, Origin, Headers) ->
+    case allow_credentials(Config, Origin) of
+        false ->
+            Headers;
+        true ->
+            Headers ++ [{"Access-Control-Allow-Credentials", "true"}]
+    end.
+
+
+allow_credentials(_Config, <<"*">>) ->
+    false;
+allow_credentials(Config, Origin) ->
+    get_origin_config(Config, Origin, <<"allow_credentials">>,
+        ?CORS_DEFAULT_ALLOW_CREDENTIALS).
+
+get_cors_config(_Req) ->
+    EnableCors = config:get("httpd", "enable_cors", "false") =:= "true",
+    AllowCredentials = config:get("cors", "credentials", "false") =:= "true",
+    AllowHeaders = case config:get("cors", "methods", undefined) of
+        undefined ->
+            ?SUPPORTED_HEADERS;
+        AllowHeaders0 ->
+            split_list(AllowHeaders0)
+    end,
+    AllowMethods = case config:get("cors", "methods", undefined) of
+        undefined ->
+            ?SUPPORTED_METHODS;
+        AllowMethods0 ->
+            split_list(AllowMethods0)
+    end,
+    Origins0 = split_list(config:get("cors", "origins", [])),
+    Origins = [{list_to_binary(O), {[]}} || O <- Origins0],
+    [
+        {<<"enable_cors">>, EnableCors},
+        {<<"allow_credentials">>, AllowCredentials},
+        {<<"allow_methods">>, AllowMethods},
+        {<<"allow_headers">>, AllowHeaders},
+        {<<"origins">>, {Origins}}
+    ].
+
+
+is_cors_enabled(Config) ->
+    couch_util:get_value(<<"enable_cors">>, Config, false).
+
+
+%% Get a list of {Origin, OriginConfig} tuples
+%% ie: get_origin_configs(Config) ->
+%% [
+%%     {<<"http://foo.com">>,
+%%         {
+%%             [
+%%                 {<<"allow_credentials">>, true},
+%%                 {<<"allow_methods">>, [<<"POST">>]}
+%%             ]
+%%         }
+%%     },
+%%     {<<"http://baz.com">>, {[]}}
+%% ]
+get_origin_configs(Config) ->
+    {Origins} = couch_util:get_value(<<"origins">>, Config, {[]}),
+    Origins.
+
+
+%% Get config for an individual Origin
+%% ie: get_origin_config(Config, <<"http://foo.com">>) ->
+%% [
+%%     {<<"allow_credentials">>, true},
+%%     {<<"allow_methods">>, [<<"POST">>]}
+%% ]
+get_origin_config(Config, Origin) ->
+    OriginConfigs = get_origin_configs(Config),
+    {OriginConfig} = couch_util:get_value(Origin, OriginConfigs, {[]}),
+    OriginConfig.
+
+
+%% Get config of a single key for an individual Origin
+%% ie: get_origin_config(Config, <<"http://foo.com">>, <<"allow_methods">>,
[])
+%% [<<"POST">>]
+get_origin_config(Config, Origin, Key, Default) ->
+    OriginConfig = get_origin_config(Config, Origin),
+    couch_util:get_value(Key, OriginConfig,
+        couch_util:get_value(Key, Config, Default)).
+
+
+get_origin(Req) ->
+    case chttpd:header_value(Req, "Origin") of
+        undefined ->
+            undefined;
+        Origin ->
+            ?l2b(string:to_lower(Origin))
+    end.
+
+
+get_accepted_origins(_Req, Config) ->
+    lists:map(fun({K,_V}) -> K end, get_origin_configs(Config)).
+
+
+split_list(S) ->
+    re:split(S, "\\s*,\\s*", [trim, {return, list}]).
+
+
+split_headers(H) ->
+    re:split(H, ",\\s*", [{return,list}, trim]).

http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/7c9fbe65/test/chttpd_cors_test.erl
----------------------------------------------------------------------
diff --git a/test/chttpd_cors_test.erl b/test/chttpd_cors_test.erl
new file mode 100644
index 0000000..87fec40
--- /dev/null
+++ b/test/chttpd_cors_test.erl
@@ -0,0 +1,519 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(chttpd_cors_test).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("chttpd/include/chttpd_cors.hrl").
+
+
+-define(DEFAULT_ORIGIN, "http://example.com").
+-define(DEFAULT_ORIGIN_HTTPS, "https://example.com").
+-define(EXPOSED_HEADERS,
+    "content-type, accept-ranges, etag, server, x-couch-request-id, " ++
+    "x-couch-update-newrev, x-couchdb-body-time").
+
+
+%% Test helpers
+
+
+default_user(Config) ->
+    Config.
+
+
+empty_cors_config() ->
+    [].
+
+
+minimal_cors_config() ->
+    [
+        {<<"enable_cors">>, true},
+        {<<"origins">>, {[]}}
+    ].
+
+
+simple_cors_config() ->
+    [
+        {<<"enable_cors">>, true},
+        {<<"origins">>, {[
+            {<<"http://example.com">>, {[]}}
+        ]}}
+    ].
+
+
+wildcard_cors_config() ->
+    [
+        {<<"enable_cors">>, true},
+        {<<"origins">>, {[
+            {<<"*">>, {[]}}
+        ]}}
+    ].
+
+
+access_control_cors_config(AllowCredentials) ->
+    [
+        {<<"enable_cors">>, true},
+        {<<"allow_credentials">>, AllowCredentials},
+        {<<"origins">>, {[
+            {<<"http://example.com">>, {[]}}
+        ]}}].
+
+
+multiple_cors_config() ->
+    [
+        {<<"enable_cors">>, true},
+        {<<"origins">>, {[
+            {<<"http://example.com">>, {[]}},
+            {<<"https://example.com">>, {[]}},
+            {<<"http://example.com:5984">>, {[]}},
+            {<<"https://example.com:5984">>, {[]}}
+        ]}}
+    ].
+
+
+mock_request(Method, Path, Headers0) ->
+    HeaderKey = "Access-Control-Request-Method",
+    Headers = case proplists:get_value(HeaderKey, Headers0, undefined) of
+        nil ->
+            proplists:delete(HeaderKey, Headers0);
+        undefined ->
+            case Method of
+                'OPTIONS' ->
+                    [{HeaderKey, atom_to_list(Method)} | Headers0];
+                _ ->
+                    Headers0
+            end;
+        _ ->
+            Headers0
+    end,
+    Headers1 = mochiweb_headers:make(Headers),
+    MochiReq = mochiweb_request:new(nil, Method, Path, {1, 1}, Headers1),
+    PathParts = [list_to_binary(chttpd:unquote(Part))
+        || Part <- string:tokens(Path, "/")],
+    #httpd{method=Method, mochi_req=MochiReq, path_parts=PathParts}.
+
+
+%% mock_config(UserProps) ->
+mock_config(Config) ->
+    %% {Config} = couch_util:get_value(<<"config">>, UserProps, {[]}),
+    %% meck:new(couch),
+    %% meck:expect(couch, version, fun() -> "version-1234" end),
+    meck:new(chttpd, [passthrough]),
+    meck:expect(chttpd, send_response, fun(_Req, Code, Headers, _Body) ->
+        {ok, mochiweb_response:new(nil, Code, mochiweb_headers:make(Headers))}
+    end),
+    Config.
+
+
+unmock_config() ->
+    %% true = meck:validate(couch),
+    %% ok = meck:unload(couch),
+    true = meck:validate(chttpd),
+    ok = meck:unload(chttpd).
+
+
+unmock_config(_) -> unmock_config().
+
+
+header(#httpd{}=Req, Key) ->
+    chttpd:header_value(Req, Key);
+header({mochiweb_response, [_, _, Headers]}, Key) ->
+    header(Headers, Key);
+header(Headers, Key) ->
+    mochiweb_headers:get_value(Key, Headers).
+
+
+string_headers(H) ->
+    string:join(H, ", ").
+
+
+assert_not_preflight_(Val) ->
+    ?_assertEqual(not_preflight, Val).
+
+
+%% CORS disabled tests
+
+
+cors_disabled_test_() ->
+    {"CORS disabled tests",
+        [
+            {"Empty user",
+                {foreach,
+                    fun() -> mock_config(empty_cors_config()) end,
+                    fun unmock_config/1,
+                    [
+                        fun test_no_access_control_method_preflight_request_/1,
+                        fun test_no_headers_/1,
+                        fun test_no_headers_server_/1,
+                        fun test_no_headers_db_/1
+                    ]}}]}.
+
+
+%% CORS enabled tests
+
+
+cors_enabled_minimal_config_test_() ->
+    {"Minimal CORS enabled, no Origins",
+        {foreach,
+            %% fun() -> mock_config(default_user(minimal_cors_config())) end,
+            fun() -> mock_config(minimal_cors_config()) end,
+            fun unmock_config/1,
+            [
+                fun test_no_access_control_method_preflight_request_/1,
+                fun test_incorrect_origin_simple_request_/1,
+                fun test_incorrect_origin_preflight_request_/1
+            ]}}.
+
+
+cors_enabled_simple_config_test_() ->
+    {"Simple CORS config",
+        {foreach,
+            fun() -> mock_config(simple_cors_config()) end,
+            fun unmock_config/1,
+            [
+                fun test_no_access_control_method_preflight_request_/1,
+                fun test_preflight_request_/1,
+                fun test_bad_headers_preflight_request_/1,
+                fun test_good_headers_preflight_request_/1,
+                fun test_db_request_/1,
+                fun test_db_preflight_request_/1,
+                fun test_db_host_origin_request_/1,
+                fun test_preflight_with_port_no_origin_/1,
+                fun test_preflight_with_scheme_no_origin_/1,
+                fun test_preflight_with_scheme_port_no_origin_/1,
+                fun test_case_sensitive_mismatch_of_allowed_origins_/1
+            ]}}.
+
+
+cors_enabled_multiple_config_test_() ->
+    {"Multiple options CORS config",
+        {foreach,
+            fun() -> mock_config(multiple_cors_config()) end,
+            fun unmock_config/1,
+            [
+                fun test_no_access_control_method_preflight_request_/1,
+                fun test_preflight_request_/1,
+                fun test_db_request_/1,
+                fun test_db_preflight_request_/1,
+                fun test_db_host_origin_request_/1,
+                fun test_preflight_with_port_with_origin_/1,
+                fun test_preflight_with_scheme_with_origin_/1,
+                fun test_preflight_with_scheme_port_with_origin_/1
+            ]}}.
+
+
+%% Access-Control-Allow-Credentials tests
+
+
+%% http://www.w3.org/TR/cors/#supports-credentials
+%% 6.1.3
+%% If the resource supports credentials add a single
+%% Access-Control-Allow-Origin header, with the value
+%% of the Origin header as value, and add a single
+%% Access-Control-Allow-Credentials header with the
+%% case-sensitive string "true" as value.
+%% Otherwise, add a single Access-Control-Allow-Origin
+%% header, with either the value of the Origin header
+%% or the string "*" as value.
+%% Note: The string "*" cannot be used for a resource
+%% that supports credentials.
+
+db_request_credentials_header_off_test_() ->
+    {"Allow credentials disabled",
+        {setup,
+            fun() ->
+                mock_config(access_control_cors_config(false))
+            end,
+            fun unmock_config/1,
+            fun test_db_request_credentials_header_off_/1
+        }
+    }.
+
+
+db_request_credentials_header_on_test_() ->
+    {"Allow credentials enabled",
+        {setup,
+            fun() ->
+                mock_config(access_control_cors_config(true))
+            end,
+            fun unmock_config/1,
+            fun test_db_request_credentials_header_on_/1
+        }
+    }.
+
+
+%% CORS wildcard tests
+
+
+cors_enabled_wildcard_test_() ->
+    {"Wildcard CORS config",
+        {foreach,
+            fun() -> mock_config(default_user(wildcard_cors_config())) end,
+            fun unmock_config/1,
+            [
+                fun test_no_access_control_method_preflight_request_/1,
+                fun test_preflight_request_/1,
+                fun test_preflight_request_no_allow_credentials_/1,
+                fun test_db_request_/1,
+                fun test_db_preflight_request_/1,
+                fun test_db_host_origin_request_/1,
+                fun test_preflight_with_port_with_origin_/1,
+                fun test_preflight_with_scheme_with_origin_/1,
+                fun test_preflight_with_scheme_port_with_origin_/1,
+                fun test_case_sensitive_mismatch_of_allowed_origins_/1
+            ]}}.
+
+
+%% Test generators
+
+
+test_no_headers_(OwnerConfig) ->
+    Req = mock_request('GET', "/", []),
+    assert_not_preflight_(chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig)).
+
+
+test_no_headers_server_(OwnerConfig) ->
+    Req = mock_request('GET', "/", [{"Origin", "http://127.0.0.1"}]),
+    assert_not_preflight_(chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig)).
+
+
+test_no_headers_db_(OwnerConfig) ->
+    Headers = [{"Origin", "http://127.0.0.1"}],
+    Req = mock_request('GET', "/my_db", Headers),
+    assert_not_preflight_(chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig)).
+
+
+test_incorrect_origin_simple_request_(OwnerConfig) ->
+    ?debugFmt("OWNER CONFIG: ~p~n", [OwnerConfig]),
+    Req = mock_request('GET', "/", [{"Origin", "http://127.0.0.1"}]),
+    [
+        ?_assert(chttpd_cors:is_cors_enabled(OwnerConfig)),
+        assert_not_preflight_(chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig))
+    ].
+
+
+test_incorrect_origin_preflight_request_(OwnerConfig) ->
+    Headers = [
+        {"Origin", "http://127.0.0.1"},
+        {"Access-Control-Request-Method", "GET"}
+    ],
+    Req = mock_request('GET', "/", Headers),
+    [
+        ?_assert(chttpd_cors:is_cors_enabled(OwnerConfig)),
+        assert_not_preflight_(chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig))
+    ].
+
+
+test_bad_headers_preflight_request_(OwnerConfig) ->
+    Headers = [
+        {"Origin", ?DEFAULT_ORIGIN},
+        {"Access-Control-Request-Method", "GET"},
+        {"Access-Control-Request-Headers", "X-Not-An-Allowed-Headers"}
+    ],
+    Req = mock_request('OPTIONS', "/", Headers),
+    [
+        ?_assert(chttpd_cors:is_cors_enabled(OwnerConfig)),
+        assert_not_preflight_(chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig))
+    ].
+
+
+test_good_headers_preflight_request_(OwnerConfig) ->
+    Headers = [
+        {"Origin", ?DEFAULT_ORIGIN},
+        {"Access-Control-Request-Method", "GET"},
+        {"Access-Control-Request-Headers", "accept-language"}
+    ],
+    Req = mock_request('OPTIONS', "/", Headers),
+    ?debugFmt("OWNER CONFIG: ~p~n HEADERS: ~p~n", [OwnerConfig, Headers]),
+    ?assert(chttpd_cors:is_cors_enabled(OwnerConfig)),
+    {ok, Resp} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Resp, "Access-Control-Allow-Origin")),
+        ?_assertEqual(string_headers(?SUPPORTED_METHODS),
+            header(Resp, "Access-Control-Allow-Methods"))
+    ].
+
+
+test_preflight_request_(OwnerConfig) ->
+    Headers = [
+        {"Origin", ?DEFAULT_ORIGIN},
+        {"Access-Control-Request-Method", "GET"}
+    ],
+    Req = mock_request('OPTIONS', "/", Headers),
+    {ok, Resp} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Resp, "Access-Control-Allow-Origin")),
+        ?_assertEqual(string_headers(?SUPPORTED_METHODS),
+            header(Resp, "Access-Control-Allow-Methods"))
+    ].
+
+
+test_no_access_control_method_preflight_request_(OwnerConfig) ->
+    Headers = [
+        {"Origin", ?DEFAULT_ORIGIN},
+        {"Access-Control-Request-Method", notnil}
+    ],
+    Req = mock_request('OPTIONS', "/", Headers),
+    assert_not_preflight_(chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig)).
+
+
+test_preflight_request_no_allow_credentials_(OwnerConfig) ->
+    Headers = [
+        {"Origin", ?DEFAULT_ORIGIN},
+        {"Access-Control-Request-Method", "GET"}
+    ],
+    Req = mock_request('OPTIONS', "/", Headers),
+    {ok, Resp} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Resp, "Access-Control-Allow-Origin")),
+        ?_assertEqual(string_headers(?SUPPORTED_METHODS),
+            header(Resp, "Access-Control-Allow-Methods")),
+        ?_assertEqual(undefined,
+            header(Resp, "Access-Control-Allow-Credentials"))
+    ].
+
+
+test_db_request_(OwnerConfig) ->
+    Origin = ?DEFAULT_ORIGIN,
+    Headers = [{"Origin", Origin}],
+    Req = mock_request('GET', "/my_db", Headers),
+    Headers1 = mochiweb_headers:make(
+        chttpd_cors:headers(Req, Headers, Origin, OwnerConfig)),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Headers1, "Access-Control-Allow-Origin")),
+        ?_assertEqual(?EXPOSED_HEADERS,
+            header(Headers1, "Access-Control-Expose-Headers"))
+    ].
+
+
+test_db_preflight_request_(OwnerConfig) ->
+    Headers = [
+        {"Origin", ?DEFAULT_ORIGIN}
+    ],
+    Req = mock_request('OPTIONS', "/my_db", Headers),
+    {ok, Resp} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Resp, "Access-Control-Allow-Origin")),
+        ?_assertEqual(string_headers(?SUPPORTED_METHODS),
+            header(Resp, "Access-Control-Allow-Methods"))
+    ].
+
+
+test_db_host_origin_request_(OwnerConfig) ->
+    Origin = ?DEFAULT_ORIGIN,
+    Headers = [
+        {"Origin", Origin},
+        {"Host", "example.com"}
+    ],
+    Req = mock_request('GET', "/my_db", Headers),
+    Headers1 = mochiweb_headers:make(
+        chttpd_cors:headers(Req, Headers, Origin, OwnerConfig)),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Headers1, "Access-Control-Allow-Origin")),
+        ?_assertEqual(?EXPOSED_HEADERS,
+            header(Headers1, "Access-Control-Expose-Headers"))
+    ].
+
+
+test_preflight_origin_helper_(OwnerConfig, Origin, ExpectedOrigin) ->
+    Headers = [
+        {"Origin", Origin},
+        {"Access-Control-Request-Method", "GET"}
+    ],
+    Req = mock_request('OPTIONS', "/", Headers),
+    Headers1 = mochiweb_headers:make(
+        chttpd_cors:headers(Req, Headers, Origin, OwnerConfig)),
+    [?_assertEqual(ExpectedOrigin,
+        header(Headers1, "Access-Control-Allow-Origin"))
+    ].
+
+
+test_preflight_with_port_no_origin_(OwnerConfig) ->
+    Origin = ?DEFAULT_ORIGIN ++ ":5984",
+    test_preflight_origin_helper_(OwnerConfig, Origin, undefined).
+
+
+test_preflight_with_port_with_origin_(OwnerConfig) ->
+    Origin = ?DEFAULT_ORIGIN ++ ":5984",
+    test_preflight_origin_helper_(OwnerConfig, Origin, Origin).
+
+
+test_preflight_with_scheme_no_origin_(OwnerConfig) ->
+    test_preflight_origin_helper_(OwnerConfig, ?DEFAULT_ORIGIN_HTTPS, undefined).
+
+
+test_preflight_with_scheme_with_origin_(OwnerConfig) ->
+    Origin = ?DEFAULT_ORIGIN_HTTPS,
+    test_preflight_origin_helper_(OwnerConfig, Origin, Origin).
+
+
+test_preflight_with_scheme_port_no_origin_(OwnerConfig) ->
+    Origin = ?DEFAULT_ORIGIN_HTTPS ++ ":5984",
+    test_preflight_origin_helper_(OwnerConfig, Origin, undefined).
+
+
+test_preflight_with_scheme_port_with_origin_(OwnerConfig) ->
+    Origin = ?DEFAULT_ORIGIN_HTTPS ++ ":5984",
+    test_preflight_origin_helper_(OwnerConfig, Origin, Origin).
+
+
+test_case_sensitive_mismatch_of_allowed_origins_(OwnerConfig) ->
+    Origin = "http://EXAMPLE.COM",
+    Headers = [{"Origin", Origin}],
+    Req = mock_request('GET', "/", Headers),
+    Headers1 = mochiweb_headers:make(
+        chttpd_cors:headers(Req, Headers, Origin, OwnerConfig)),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Headers1, "Access-Control-Allow-Origin")),
+        ?_assertEqual(?EXPOSED_HEADERS,
+            header(Headers1, "Access-Control-Expose-Headers"))
+    ].
+
+
+test_db_request_credentials_header_off_(OwnerConfig) ->
+    ?debugFmt("DB REQ OWNER CONF: ~p~n", [OwnerConfig]),
+    Origin = ?DEFAULT_ORIGIN,
+    Headers = [{"Origin", Origin}],
+    Req = mock_request('GET', "/", Headers),
+    Headers1 = mochiweb_headers:make(
+        chttpd_cors:headers(Req, Headers, Origin, OwnerConfig)),
+    ?debugFmt("ACTUAL HEADERS: ~p~n", [Headers1]),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Headers1, "Access-Control-Allow-Origin")),
+        ?_assertEqual(undefined,
+            header(Headers1, "Access-Control-Allow-Credentials"))
+    ].
+
+
+test_db_request_credentials_header_on_(OwnerConfig) ->
+    Origin = ?DEFAULT_ORIGIN,
+    Headers = [{"Origin", Origin}],
+    Req = mock_request('GET', "/", Headers),
+    Headers1 = mochiweb_headers:make(
+        chttpd_cors:headers(Req, Headers, Origin, OwnerConfig)),
+    ?debugFmt("ACTUAL HEADERS: ~p~n", [Headers1]),
+    [
+        ?_assertEqual(?DEFAULT_ORIGIN,
+            header(Headers1, "Access-Control-Allow-Origin")),
+        ?_assertEqual("true",
+            header(Headers1, "Access-Control-Allow-Credentials"))
+    ].


Mime
View raw message