From 3cf2b469fa590fac77d95bc1805f249b89e97df3 Mon Sep 17 00:00:00 2001 From: Russell Branca Date: Tue, 4 Nov 2014 16:07:38 -0800 Subject: [PATCH 1/2] CORS implementation for chttpd --- include/chttpd_cors.hrl | 81 +++++++ src/chttpd.erl | 4 +- src/chttpd_cors.erl | 344 ++++++++++++++++++++++++++- test/chttpd_cors_test.erl | 475 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 897 insertions(+), 7 deletions(-) create mode 100644 include/chttpd_cors.hrl create mode 100644 test/chttpd_cors_test.erl 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 @@ 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), 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")) + ]. From e2c2bd7ba4ef3124c1f4fae6d30748764fd4271c Mon Sep 17 00:00:00 2001 From: Russell Branca Date: Thu, 6 Nov 2014 14:48:51 -0800 Subject: [PATCH 2/2] Disable couch_httpd_cors when chttpd_cors is active --- src/chttpd_cors.erl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/chttpd_cors.erl b/src/chttpd_cors.erl index e0e8fd0..b109b45 100644 --- a/src/chttpd_cors.erl +++ b/src/chttpd_cors.erl @@ -288,6 +288,12 @@ get_cors_config(_Req) -> is_cors_enabled(Config) -> + case get(disable_couch_httpd_cors) of + undefined -> + put(disable_couch_httpd_cors, true); + _ -> + ok + end, couch_util:get_value(<<"enable_cors">>, Config, false).