CORS implementation for chttpd
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).
diff --git a/src/chttpd.erl b/src/chttpd.erl
index 32aa1fc..b23e131 100644
--- a/src/chttpd.erl
+++ b/src/chttpd.erl
@@ -203,8 +203,8 @@
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),
diff --git a/src/chttpd_cors.erl b/src/chttpd_cors.erl
index 03ec289..e0e8fd0 100644
--- a/src/chttpd_cors.erl
+++ b/src/chttpd_cors.erl
@@ -12,10 +12,344 @@
-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) ->
+ case maybe_handle_preflight_request(Req, get_cors_config(Req)) of
+ not_preflight ->
+ not_preflight;
+ {ok, PreflightHeaders} ->
+ chttpd:send_response(Req, 204, PreflightHeaders, <<>>)
+ end.
+
+
+maybe_handle_preflight_request(#httpd{}=Req, Config) ->
+ case is_cors_enabled(Config) of
+ true ->
+ case preflight_request(Req, Config) of
+ {ok, PreflightHeaders} ->
+ {ok, 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", binary_to_list(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", binary_to_list(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 = binary_split_list(config:get("cors", "origins", [])),
+ Origins = [{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 ->
+ list_to_binary(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}]).
+
+
+binary_split_list(S) ->
+ [list_to_binary(E) || E <- split_list(S)].
+
+
+split_headers(H) ->
+ re:split(H, ",\\s*", [{return,list}, trim]).
diff --git a/test/chttpd_cors_test.erl b/test/chttpd_cors_test.erl
new file mode 100644
index 0000000..6ad807a
--- /dev/null
+++ b/test/chttpd_cors_test.erl
@@ -0,0 +1,475 @@
+% 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
+
+
+empty_cors_config() ->
+ [].
+
+
+minimal_cors_config() ->
+ [
+ {<<"enable_cors">>, true},
+ {<<"origins">>, {[]}}
+ ].
+
+
+simple_cors_config() ->
+ [
+ {<<"enable_cors">>, true},
+ {<<"origins">>, {[
+ {list_to_binary(?DEFAULT_ORIGIN), {[]}}
+ ]}}
+ ].
+
+
+wildcard_cors_config() ->
+ [
+ {<<"enable_cors">>, true},
+ {<<"origins">>, {[
+ {<<"*">>, {[]}}
+ ]}}
+ ].
+
+
+access_control_cors_config(AllowCredentials) ->
+ [
+ {<<"enable_cors">>, true},
+ {<<"allow_credentials">>, AllowCredentials},
+ {<<"origins">>, {[
+ {list_to_binary(?DEFAULT_ORIGIN), {[]}}
+ ]}}].
+
+
+multiple_cors_config() ->
+ [
+ {<<"enable_cors">>, true},
+ {<<"origins">>, {[
+ {list_to_binary(?DEFAULT_ORIGIN), {[]}},
+ {<<"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}.
+
+
+header(#httpd{}=Req, Key) ->
+ chttpd:header_value(Req, Key);
+header({mochiweb_response, [_, _, Headers]}, Key) ->
+ %% header(Headers, Key);
+ mochiweb_headers:get_value(Key, Headers);
+header(Headers, Key) ->
+ couch_util:get_value(Key, Headers, undefined).
+
+
+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 empty_cors_config/0,
+ [
+ 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 minimal_cors_config/0,
+ [
+ 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 simple_cors_config/0,
+ [
+ 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 multiple_cors_config/0,
+ [
+ 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() ->
+ access_control_cors_config(false)
+ end,
+ fun test_db_request_credentials_header_off_/1
+ }
+ }.
+
+
+db_request_credentials_header_on_test_() ->
+ {"Allow credentials enabled",
+ {setup,
+ fun() ->
+ access_control_cors_config(true)
+ end,
+ fun test_db_request_credentials_header_on_/1
+ }
+ }.
+
+
+%% CORS wildcard tests
+
+
+cors_enabled_wildcard_test_() ->
+ {"Wildcard CORS config",
+ {foreach,
+ fun wildcard_cors_config/0,
+ [
+ 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) ->
+ 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),
+ ?assert(chttpd_cors:is_cors_enabled(OwnerConfig)),
+ {ok, Headers1} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig),
+ [
+ ?_assertEqual(?DEFAULT_ORIGIN,
+ header(Headers1, "Access-Control-Allow-Origin")),
+ ?_assertEqual(string_headers(?SUPPORTED_METHODS),
+ header(Headers1, "Access-Control-Allow-Methods"))
+ ].
+
+
+test_preflight_request_(OwnerConfig) ->
+ Headers = [
+ {"Origin", ?DEFAULT_ORIGIN},
+ {"Access-Control-Request-Method", "GET"}
+ ],
+ Req = mock_request('OPTIONS', "/", Headers),
+ {ok, Headers1} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig),
+ [
+ ?_assertEqual(?DEFAULT_ORIGIN,
+ header(Headers1, "Access-Control-Allow-Origin")),
+ ?_assertEqual(string_headers(?SUPPORTED_METHODS),
+ header(Headers1, "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, Headers1} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig),
+ [
+ ?_assertEqual(?DEFAULT_ORIGIN,
+ header(Headers1, "Access-Control-Allow-Origin")),
+ ?_assertEqual(string_headers(?SUPPORTED_METHODS),
+ header(Headers1, "Access-Control-Allow-Methods")),
+ ?_assertEqual(undefined,
+ header(Headers1, "Access-Control-Allow-Credentials"))
+ ].
+
+
+test_db_request_(OwnerConfig) ->
+ Origin = ?DEFAULT_ORIGIN,
+ Headers = [{"Origin", Origin}],
+ Req = mock_request('GET', "/my_db", Headers),
+ Headers1 = 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, Headers1} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig),
+ [
+ ?_assertEqual(?DEFAULT_ORIGIN,
+ header(Headers1, "Access-Control-Allow-Origin")),
+ ?_assertEqual(string_headers(?SUPPORTED_METHODS),
+ header(Headers1, "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 = 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 = 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 = 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) ->
+ Origin = ?DEFAULT_ORIGIN,
+ Headers = [{"Origin", Origin}],
+ Req = mock_request('GET', "/", Headers),
+ Headers1 = chttpd_cors:headers(Req, Headers, Origin, OwnerConfig),
+ [
+ ?_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 = chttpd_cors:headers(Req, Headers, Origin, OwnerConfig),
+ [
+ ?_assertEqual(?DEFAULT_ORIGIN,
+ header(Headers1, "Access-Control-Allow-Origin")),
+ ?_assertEqual("true",
+ header(Headers1, "Access-Control-Allow-Credentials"))
+ ].