diff options
Diffstat (limited to 'src/chttpd/test/eunit/chttpd_cors_test.erl')
-rw-r--r-- | src/chttpd/test/eunit/chttpd_cors_test.erl | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/src/chttpd/test/eunit/chttpd_cors_test.erl b/src/chttpd/test/eunit/chttpd_cors_test.erl new file mode 100644 index 000000000..19e851561 --- /dev/null +++ b/src/chttpd/test/eunit/chttpd_cors_test.erl @@ -0,0 +1,564 @@ +% 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"). + +-define(CUSTOM_SUPPORTED_METHODS, ?SUPPORTED_METHODS -- ["CONNECT"]). +-define(CUSTOM_SUPPORTED_HEADERS, ["extra" | ?SUPPORTED_HEADERS -- ["pragma"]]). +-define(CUSTOM_EXPOSED_HEADERS, ["expose" | ?COUCH_HEADERS]). + +-define(CUSTOM_MAX_AGE, round(?CORS_DEFAULT_MAX_AGE / 2)). + +%% 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">>, {[ + {<<"*">>, {[]}} + ]}} + ]. + +custom_cors_config() -> + [ + {<<"enable_cors">>, true}, + {<<"allow_methods">>, ?CUSTOM_SUPPORTED_METHODS}, + {<<"allow_headers">>, ?CUSTOM_SUPPORTED_HEADERS}, + {<<"exposed_headers">>, ?CUSTOM_EXPOSED_HEADERS}, + {<<"max_age">>, ?CUSTOM_MAX_AGE}, + {<<"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_custom_config_test_() -> + {"Simple CORS config with custom allow_methods/allow_headers/exposed_headers", + {foreach, + fun custom_cors_config/0, + [ + fun test_good_headers_preflight_request_with_custom_config_/1, + fun test_db_request_with_custom_config_/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_preflight_request_empty_request_headers_/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")), + ?_assertEqual(string_headers(["accept-language"]), + header(Headers1, "Access-Control-Allow-Headers")) + ]. + +test_good_headers_preflight_request_with_custom_config_(OwnerConfig) -> + Headers = [ + {"Origin", ?DEFAULT_ORIGIN}, + {"Access-Control-Request-Method", "GET"}, + {"Access-Control-Request-Headers", "accept-language, extra"}, + {"Access-Control-Max-Age", ?CORS_DEFAULT_MAX_AGE} + ], + Req = mock_request('OPTIONS', "/", Headers), + ?assert(chttpd_cors:is_cors_enabled(OwnerConfig)), + AllowMethods = couch_util:get_value( + <<"allow_methods">>, OwnerConfig, ?SUPPORTED_METHODS), + MaxAge = couch_util:get_value( + <<"max_age">>, OwnerConfig, ?CORS_DEFAULT_MAX_AGE), + {ok, Headers1} = chttpd_cors:maybe_handle_preflight_request(Req, OwnerConfig), + [ + ?_assertEqual(?DEFAULT_ORIGIN, + header(Headers1, "Access-Control-Allow-Origin")), + ?_assertEqual(string_headers(AllowMethods), + header(Headers1, "Access-Control-Allow-Methods")), + ?_assertEqual(string_headers(["accept-language", "extra"]), + header(Headers1, "Access-Control-Allow-Headers")), + ?_assertEqual(MaxAge, + header(Headers1, "Access-Control-Max-Age")) + ]. + + +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_preflight_request_empty_request_headers_(OwnerConfig) -> + Headers = [ + {"Origin", ?DEFAULT_ORIGIN}, + {"Access-Control-Request-Method", "POST"}, + {"Access-Control-Request-Headers", ""} + ], + 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("", + header(Headers1, "Access-Control-Allow-Headers")) + ]. + + +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_request_with_custom_config_(OwnerConfig) -> + Origin = ?DEFAULT_ORIGIN, + Headers = [{"Origin", Origin}, {"extra", "EXTRA"}], + Req = mock_request('GET', "/my_db", Headers), + Headers1 = chttpd_cors:headers(Req, Headers, Origin, OwnerConfig), + ExposedHeaders = couch_util:get_value( + <<"exposed_headers">>, OwnerConfig, ?COUCH_HEADERS), + [ + ?_assertEqual(?DEFAULT_ORIGIN, + header(Headers1, "Access-Control-Allow-Origin")), + ?_assertEqual(lists:sort(["content-type" | ExposedHeaders]), + lists:sort( + split_list(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")) + ]. + +split_list(S) -> + re:split(S, "\\s*,\\s*", [trim, {return, list}]). |