Repository: couchdb-chttpd Updated Branches: refs/heads/2080-port-cors [created] b420d5055
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/b420d505 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/tree/b420d505 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/diff/b420d505 Branch: refs/heads/2080-port-cors Commit: b420d505592683256e0249d82aeed8f5a783c5ea Parents: b44515f Author: Russell Branca <[email protected]> Authored: Tue Nov 4 16:07:38 2014 -0800 Committer: Russell Branca <[email protected]> Committed: Tue Nov 4 16:09:39 2014 -0800 ---------------------------------------------------------------------- include/chttpd_cors.hrl | 81 +++++++ src/chttpd.erl | 4 +- src/chttpd_cors.erl | 332 +++++++++++++++++++++++++- test/chttpd_cors_test.erl | 519 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 929 insertions(+), 7 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/b420d505/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/b420d505/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/b420d505/src/chttpd_cors.erl ---------------------------------------------------------------------- diff --git a/src/chttpd_cors.erl b/src/chttpd_cors.erl index 03ec289..6bc037b 100644 --- a/src/chttpd_cors.erl +++ b/src/chttpd_cors.erl @@ -12,10 +12,332 @@ -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 = case get_origin_config(Config, Origin, + <<"allow_methods">>, undefined) of + undefined -> + ?SUPPORTED_METHODS; + Methods -> + [?b2l(M) || M <- Methods] + end, + + %% 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 = + split_list(config:get("cors", "methods", ?SUPPORTED_HEADERS)), + AllowMethods = + split_list(config:get("cors", "methods", ?SUPPORTED_METHODS)), + 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/b420d505/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")) + ].
