From bedccc8ef801a0516ffa48eb11e43dd9616392b3 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Mon, 16 Jan 2012 13:17:03 +0000 Subject: [PATCH 01/29] The new spec, with some notes from me --- spec.txt | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 spec.txt diff --git a/spec.txt b/spec.txt new file mode 100644 index 00000000000..a8a2c31bad4 --- /dev/null +++ b/spec.txt @@ -0,0 +1,76 @@ +TODO +==== + +* Interaction of _security and global config +* Multiple origins, the spec says "list of origins" +* w3c spec S 5.1.4. Make sure that couch never exposes non-simple headers that are not in AC-Expose-Headers. +* Different tests for simple (s5.1) vs. preflight (s5.2) + +guidelines : +---------- + +- rules should be based on host +- rules depending on the resource : + - server : rules defined in .ini + - db : rules defined in .db + +- default cors policy (open for discussion) + - allows credential = false + - cors enabled +- cors can be disabled globally + + +rules definiton : + +global wide + +[httpd] +cors_enabled = true + +[origins] +domain.tld = http://origin.tld, https://origin.tld + +[http://origin.tld] +allow_methods = GET, POST +allow_headers = x-couchdb-... +allow_credentials = false + + +[https://origin.tld] +allow_methods = GET, PUT, POST, DELETE +allow_headers = x-couchdb-... +allow_credentials = true +allow_server_admins = true +max-age = 36000 + + +on the db _security object : +{ + "origins": { + "domain.tld": [ + {"http://origin.tld": { "allow_methods": "GET, POST", +...} + ] + } +} + + +work flow (run for request handling, and again after any rewrite): + +for /db resources, including system dbs, use the db _security object +for all other resources (e.g. /_uuids), or when there is no _security object, use the ini configuration +is the 'origins' section empty or non-existant ? +yes -> is admin party set ? + yes -> return "*" , credentials false (with a good caching policy) + no -> stop +no -> + run the following steps [apply cors steps] + is Host in 'origins' ? + yes -> + is Origin in 'origins[Host]' ? + yes -> + set the cors headers based on 'origins[Host]' + no -> fail + no -> + + From 1da5a293e31d65a33ab2099e9eef9a9e47f97d3b Mon Sep 17 00:00:00 2001 From: "Jason Smith (air)" Date: Fri, 13 Jan 2012 03:38:38 +0000 Subject: [PATCH 02/29] A module to apply the CORS policy to a given request --- etc/couchdb/default.ini.tpl.in | 1 + etc/couchdb/local.ini | 18 +++++++ src/couchdb/Makefile.am | 2 + src/couchdb/couch_cors_policy.erl | 21 ++++++++ test/etap/250-cors-policy.t | 82 +++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+) create mode 100644 src/couchdb/couch_cors_policy.erl create mode 100755 test/etap/250-cors-policy.t diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index ce849057fc2..f247cc4a3cc 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -44,6 +44,7 @@ default_handler = {couch_httpd_db, handle_request} secure_rewrites = true vhost_global_handlers = _utils, _uuids, _session, _oauth, _users allow_jsonp = false +cors_enabled = false ; Options for the MochiWeb HTTP server. ;server_options = [{backlog, 128}, {acceptor_pool_size, 16}] ; For more socket options, consult Erlang's module 'inet' man page. diff --git a/etc/couchdb/local.ini b/etc/couchdb/local.ini index 9e711e179de..0a0cccae41c 100644 --- a/etc/couchdb/local.ini +++ b/etc/couchdb/local.ini @@ -15,6 +15,11 @@ ; For more socket options, consult Erlang's module 'inet' man page. ;socket_options = [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, true}] +; Set this to true to enable cross-origin resource sharing (CORS). Next, +; grant individual domains access to this server by adding them to +; the [origins] section. +;cors_enabled = true + ; Uncomment next line to trigger basic-auth popup on unauthorized requests. ;WWW-Authenticate = Basic realm="administrator" @@ -25,6 +30,19 @@ ; the whitelist. ;config_whitelist = [{httpd,config_whitelist}, {log,level}, {etc,etc}] +[origins] +; Place this server's domain on the left-hand side, and a comma-separated +; list of allowed origins on the right hand side. +;example.com:5984 = http://couchdb.apache.org, https://couchdb.apache.org:6984 + +; Optionally, override the default CORS response with a section for the origin. +; For example, jQuery v1.5.1 uses an X-Requested-With header. +;[http://couchdb.apache.org] +;allow_methods = GET, POST, PUT +;allow_headers = X-CouchDB-WWW-Authenticate, X-Requested-With +;allow_credentials = false +;max_age = 36000 + [httpd_global_handlers] ;_google = {couch_httpd_proxy, handle_proxy_req, <<"http://www.google.com">>} diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am index 57059764852..68b7dd4e67c 100644 --- a/src/couchdb/Makefile.am +++ b/src/couchdb/Makefile.am @@ -36,6 +36,7 @@ source_files = \ couch_compress.erl \ couch_config.erl \ couch_config_writer.erl \ + couch_cors_policy.erl \ couch_db.erl \ couch_db_update_notifier.erl \ couch_db_update_notifier_sup.erl \ @@ -92,6 +93,7 @@ compiled_files = \ couch_compress.beam \ couch_config.beam \ couch_config_writer.beam \ + couch_cors_policy.beam \ couch_db.beam \ couch_db_update_notifier.beam \ couch_db_update_notifier_sup.beam \ diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl new file mode 100644 index 00000000000..b741fce6c9c --- /dev/null +++ b/src/couchdb/couch_cors_policy.erl @@ -0,0 +1,21 @@ +% 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(couch_cors_policy). +-export([check/3]). + +-include("couch_db.hrl"). + + +check(Global, Local, #httpd{}=Req) + when is_list(Global) andalso is_list(Local) -> + ok. diff --git a/test/etap/250-cors-policy.t b/test/etap/250-cors-policy.t new file mode 100755 index 00000000000..3e79e774d30 --- /dev/null +++ b/test/etap/250-cors-policy.t @@ -0,0 +1,82 @@ +#!/usr/bin/env escript +% 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. + +% Test only the policy decisions of couch_cors_policy:check/3--no servers +% or other couch_* stuff. + +-record(httpd, + {mochi_req, + peer, + method, + requested_path_parts, + path_parts, + db_url_handlers, + user_ctx, + req_body = undefined, + design_url_handlers, + auth, + default_fun, + url_handlers + }). + +main(_) -> + test_util:init_code_path(), + + etap:plan(7), + case (catch test()) of + ok -> + etap:end_tests(); + Other -> + etap:diag(io_lib:format("Test died abnormally: ~p", [Other])), + etap:bail(Other) + end, + ok. + +test() -> + test_bad_api_calls(), + test_good_api_calls(), + ok. + +test_bad_api_calls() -> + etap_threw(fun() -> couch_cors_policy:check() end, + true, "Policy check with zero parameters"), + etap_threw(fun() -> couch_cors_policy:check([]) end, + true, "Policy check with one parameter"), + etap_threw(fun() -> couch_cors_policy:check([], []) end, + true, "Policy check with two parameters"), + + etap_threw(fun() -> couch_cors_policy:check(notList, [], #httpd{}) end, + true, "Policy check with non-list first parameter"), + etap_threw(fun() -> couch_cors_policy:check([], notList, #httpd{}) end, + true, "Policy check with non-list second parameter"), + etap_threw(fun() -> couch_cors_policy:check([], [], {not_httpd}) end, + true, "Policy check with non-#httpd{} third parameter"), + ok. + +test_good_api_calls() -> + etap_threw(fun() -> couch_cors_policy:check([], [], #httpd{}) end, + false, "Policy check with three valid parameters"), + ok. + +% +% Utilities +% + +etap_threw(Function, Expected, Description) -> + Result = try Function() of + _NoThrow -> false + catch _:_ -> true + end, + etap:is(Result, Expected, Description). + +% vim: sts=4 sw=4 et From 96da83114a1fb524c5b0a8cdcdc88238370d68bd Mon Sep 17 00:00:00 2001 From: "Jason Smith (air)" Date: Mon, 16 Jan 2012 08:04:23 +0700 Subject: [PATCH 03/29] Build a _security-style CORS object from couch_config --- src/couchdb/couch_cors_policy.erl | 34 +++++++++- test/etap/251-cors-config.t | 104 ++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100755 test/etap/251-cors-config.t diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index b741fce6c9c..cd03c5125cd 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -11,11 +11,43 @@ % the License. -module(couch_cors_policy). --export([check/3]). +-export([global_config/0, check/2, check/3]). -include("couch_db.hrl"). +check(DbConfig, #httpd{}=Req) -> + check(global_config(), DbConfig, Req). check(Global, Local, #httpd{}=Req) when is_list(Global) andalso is_list(Local) -> ok. + + +global_config() -> + % Return the globally-configured CORS settings in a format identical + % to that in the _security object. + Enabled = couch_util:to_existing_atom( + couch_config:get("httpd", "cors_enabled", "false")), + DomainsToOrigins = binary_section("origins"), + Global = [ {<<"httpd">>, {[ {<<"cors_enabled">>, Enabled} ]}} + , {<<"origins">>, {DomainsToOrigins}} + ], + + AllOrigins = lists:flatten([ re:split(Line, ",\\s*") + || {_Vhost, Line} <- DomainsToOrigins]), + + lists:foldl(fun(OriginName, Config) -> + Stanza = {OriginName, binary_section(OriginName)}, + lists:keystore(OriginName, 1, Config, Stanza) + end, Global, AllOrigins). + +binary_section(Section) -> + % Return a config section, with all strings converted to binaries. + SectionStr = couch_config:get(Section), + BoolOrBinary = fun("true") -> true; + ("false") -> false; + (Val) -> ?l2b(Val) + end, + [ {?l2b(Key), BoolOrBinary(Val)} || {Key, Val} <- SectionStr ]. + +% vim: sts=4 sw=4 et diff --git a/test/etap/251-cors-config.t b/test/etap/251-cors-config.t new file mode 100755 index 00000000000..f3a45391db5 --- /dev/null +++ b/test/etap/251-cors-config.t @@ -0,0 +1,104 @@ +#!/usr/bin/env escript +% 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. + +-record(httpd, + {mochi_req, + peer, + method, + requested_path_parts, + path_parts, + db_url_handlers, + user_ctx, + req_body = undefined, + design_url_handlers, + auth, + default_fun, + url_handlers + }). + +default_config() -> + test_util:build_file("etc/couchdb/default_dev.ini"). + +main(_) -> + test_util:init_code_path(), + + etap:plan(11), + case (catch test()) of + ok -> + etap:end_tests(); + Other -> + etap:diag(io_lib:format("Test died abnormally: ~p", [Other])), + etap:bail(Other) + end, + ok. + +test() -> + % start couch_config with default + couch_config:start_link([default_config()]), + test_api_calls(), + test_policy_structure(), + ok. + +test_api_calls() -> + etap:is(threw(fun() -> couch_cors_policy:global_config() end), + false, "No problem building global CORS policy"), + + % Test the "shortcut" policy check call, which requires couch_config. + etap:not_ok(threw(fun() -> couch_cors_policy:check([], #httpd{}) end), + "Policy check with two valid parameters"), + ok. + +test_policy_structure() -> + couch_config:set("origins", "example.com", + "http://origin.com, https://origin.com:6984", false), + + Config = couch_cors_policy:global_config(), + etap:ok(is_list(Config), "Global CORS config is a list"), + + HttpdObj = couch_util:get_value(<<"httpd">>, Config), + etap:ok(is_tuple(HttpdObj), "Global CORS config: httpd section"), + + {Httpd} = HttpdObj, + etap:ok(is_list(Httpd), "Global CORS httpd section looks good"), + + Enabled = couch_util:get_value(<<"cors_enabled">>, Httpd), + etap:ok(is_boolean(Enabled), "Boolean global CORS enabled flag"), + + OriginsObj = couch_util:get_value(<<"origins">>, Config), + etap:ok(is_tuple(OriginsObj), "Global CORS config: origins section"), + + {Origins} = OriginsObj, + etap:ok(is_list(Origins), "Global CORS origins section looks good"), + + Example = couch_util:get_value(<<"example.com">>, Origins), + etap:is(Example, <<"http://origin.com, https://origin.com:6984">>, + "Global CORS origin: example.com"), + + Origin1 = couch_util:get_value(<<"http://origin.com">>, Config), + etap:ok(is_list(Origin1), "CORS origin stanza: http://origin.com"), + + Origin2 = couch_util:get_value(<<"https://origin.com:6984">>, Config), + etap:ok(is_list(Origin2), "CORS origin stanza: https://origin.com:6984"), + + ok. +% +% Utilities +% + +threw(Function) -> + try Function() of + _Nope -> false + catch _:_ -> true + end. + +% vim: sts=4 sw=4 et From e49b12b4dedf05c0ffaf40a23edce5a8442d8bba Mon Sep 17 00:00:00 2001 From: "Jason Smith (air)" Date: Mon, 16 Jan 2012 01:59:47 +0000 Subject: [PATCH 04/29] Helper code to build CORS policies and requests, keeping the actual test code clean --- test/etap/250-cors-policy.t | 83 +++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/test/etap/250-cors-policy.t b/test/etap/250-cors-policy.t index 3e79e774d30..569730baa67 100755 --- a/test/etap/250-cors-policy.t +++ b/test/etap/250-cors-policy.t @@ -66,12 +66,25 @@ test_bad_api_calls() -> test_good_api_calls() -> etap_threw(fun() -> couch_cors_policy:check([], [], #httpd{}) end, false, "Policy check with three valid parameters"), + + etap_threw(fun() -> + couch_cors_policy:check(config(), config(), #httpd{}) + end, false, "Policy check with noop configs"), + + % And the shortcut function. + etap_threw(fun() -> check([], [], req()) end, + false, "Policy check with three valid parameters"), + etap_threw(fun() -> check(config(), config(), req()) end, + false, "Policy check with noop configs"), ok. % % Utilities % +check(A, B, C) -> + couch_cors_policy:check(A, B, C). + etap_threw(Function, Expected, Description) -> Result = try Function() of _NoThrow -> false @@ -79,4 +92,74 @@ etap_threw(Function, Expected, Description) -> end, etap:is(Result, Expected, Description). +httpd() -> + httpd('GET'). + +httpd(Method) when is_atom(Method) -> + httpd(Method, "/db/doc"); + +httpd(Path) -> + httpd('GET', Path). + +httpd(Method, Path) -> + Parts = [ list_to_binary(Part) || Part <- string:tokens(Path, "/") ], + #httpd{method=Method, requested_path_parts=Parts, path_parts=Parts}. + +req() -> + req(httpd()). +req(Origin) when is_list(Origin) -> + req(httpd(), Origin); +req(Httpd) -> + req(Httpd, "http://origin.com"). +req(#httpd{method=Method, path_parts=Parts}=Req, Origin) -> + % Give this request the Origin. + Method = Req#httpd.method, + Path = filename:join(Parts), + Version = {1,1}, + Headers = mochiweb_headers:make([{"Origin", Origin}]), + MochiReq = mochiweb_request:new(nil, Method, Path, Version, Headers), + Req#httpd{ mochi_req=MochiReq }. + +% Example, CORS enabled, mydomain.com allows http://origin.com with a max-age: +% config([enabled, +% {"mydomain.com","http://origin.com"}, +% ["http://origin.com", {"max-age",3600}]]) +config() -> + config([]). +config(Opts) -> + config(Opts, []). +config([], Config) -> + Config; + +config([enabled | Opts], Config) -> + Config1 = lists:keystore(<<"httpd">>, 1, Config, + {<<"httpd">>, {[ {<<"cors_enabled">>,true} ]}}), + config(Opts, Config1); + +config([ {Dom, Orig} | Opts ], Config) -> + Domain = list_to_binary(Dom), + Origin = list_to_binary(Orig), + {Origins} = case lists:keyfind(<<"origins">>, 1, Config) of + {<<"origins">>, FoundOrigins} -> FoundOrigins; + false -> {[]} + end, + Origins1 = lists:keystore(Domain, 1, Origins, {Domain, Origin}), + Config1 = lists:keystore(<<"origins">>, 1, Config, {<<"origins">>, {Origins1}}), + config(Opts, Config1); + +config([ [Orig|KVs] | Opts ], Config) -> + Origin = list_to_binary(Orig), + Configs = lists:foldl(fun({KeyStr, ValStr}, OriginCfg) -> + Key = list_to_binary(KeyStr), + Val = case ValStr of + "true" -> true; + "false" -> false; + Num when is_number(Num) -> Num; + _ -> list_to_binary(ValStr) + end, + lists:keystore(Key, 1, OriginCfg, {Key, Val}) + end, [], KVs), + Config1 = lists:keystore(Origin, 1, Config, {Origin, {Configs}}), + config(Opts, Config1). + % vim: sts=4 sw=4 et From 91e0760497277da70cf24b4eecd75dc31ec3b34f Mon Sep 17 00:00:00 2001 From: "Jason Smith (air)" Date: Mon, 16 Jan 2012 04:28:22 +0000 Subject: [PATCH 05/29] Basic tests confirming when CORS is disabled --- src/couchdb/couch_cors_policy.erl | 7 +++++- test/etap/250-cors-policy.t | 40 ++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index cd03c5125cd..bee6abc3e25 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -20,7 +20,12 @@ check(DbConfig, #httpd{}=Req) -> check(Global, Local, #httpd{}=Req) when is_list(Global) andalso is_list(Local) -> - ok. + {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), + Enabled = couch_util:get_value(<<"cors_enabled">>, Httpd, false), + case Enabled of + true -> []; + _ -> false + end. global_config() -> diff --git a/test/etap/250-cors-policy.t b/test/etap/250-cors-policy.t index 569730baa67..e553c947181 100755 --- a/test/etap/250-cors-policy.t +++ b/test/etap/250-cors-policy.t @@ -32,7 +32,7 @@ main(_) -> test_util:init_code_path(), - etap:plan(7), + etap:plan(18), case (catch test()) of ok -> etap:end_tests(); @@ -45,6 +45,9 @@ main(_) -> test() -> test_bad_api_calls(), test_good_api_calls(), + test_disabling(), + test_enabled(), + test_duels(), ok. test_bad_api_calls() -> @@ -78,6 +81,41 @@ test_good_api_calls() -> false, "Policy check with noop configs"), ok. +test_disabling() -> + Default = config(), + Enabled = config([enabled]), % Enabled only, nothing else. + Config = config([enabled, {"example.com", "origin.com"}]), + Deactivated = config([{"example.com", "origin.com"}]), + + etap:is(check(Default, Default, req()), + false, "By default, CORS is disabled"), + etap:is(check(Default, Config, req()), + false, "By default, CORS is disabled, despite _security"), + etap:is(check(Deactivated, Default, req()), + false, "Globally disabling CORS overrides everything else"), + etap:is(check(Deactivated, Config, req()), + false, "Deactivated CORS still overrides _security"), + + etap:isnt(check(Enabled, Config, req()), + false, "Globally enabled CORS, config in _security: passes"), + etap:isnt(check(Config, Default, req()), + false, "Global CORS config, nothing in _security: passes"), + ok. + +test_enabled() -> + Enabled = config([enabled]), + Config = config([enabled, {"example.com","origin.com"}]), + etap:ok(is_list(check(Enabled, Config, req())), + "Good CORS from _security returns a list"), + etap:ok(is_list(check(Config, [], req())), + "Good CORS from _config returns a list"), + ok. + +test_duels() -> + % Configs from _security and _config have a duel! + %TODO + ok. + % % Utilities % From de431db63a969c05be50814780eccea9af9d1ba8 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Mon, 16 Jan 2012 06:52:51 +0000 Subject: [PATCH 06/29] Included the X-Forwarded-Host value when looking up the config --- src/couchdb/couch_cors_policy.erl | 5 ++++- test/etap/251-cors-config.t | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index bee6abc3e25..f171385d885 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -33,8 +33,11 @@ global_config() -> % to that in the _security object. Enabled = couch_util:to_existing_atom( couch_config:get("httpd", "cors_enabled", "false")), + XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"), DomainsToOrigins = binary_section("origins"), - Global = [ {<<"httpd">>, {[ {<<"cors_enabled">>, Enabled} ]}} + Global = [ {<<"httpd">>, {[ {<<"cors_enabled">>, Enabled} + , {<<"x_forwarded_host">>, XHost} + ]}} , {<<"origins">>, {DomainsToOrigins}} ], diff --git a/test/etap/251-cors-config.t b/test/etap/251-cors-config.t index f3a45391db5..5e8dbb65fb4 100755 --- a/test/etap/251-cors-config.t +++ b/test/etap/251-cors-config.t @@ -32,7 +32,7 @@ default_config() -> main(_) -> test_util:init_code_path(), - etap:plan(11), + etap:plan(12), case (catch test()) of ok -> etap:end_tests(); @@ -74,6 +74,10 @@ test_policy_structure() -> Enabled = couch_util:get_value(<<"cors_enabled">>, Httpd), etap:ok(is_boolean(Enabled), "Boolean global CORS enabled flag"), + XHost = couch_util:get_value(<<"x_forwarded_host">>, Httpd), + etap:is(XHost, "X-Forwarded-Host", + "CORS config has X-Forwarded-Host setting"), + OriginsObj = couch_util:get_value(<<"origins">>, Config), etap:ok(is_tuple(OriginsObj), "Global CORS config: origins section"), From b9b08e05ec91757bd79122742f3baa756f658e0d Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Mon, 16 Jan 2012 10:10:10 +0000 Subject: [PATCH 07/29] Move the policy settings *inside* the config object, like _security does --- src/couchdb/couch_cors_policy.erl | 50 +++++++++++++++++++++---------- test/etap/251-cors-config.t | 24 +++++++++------ 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index f171385d885..a55b8399116 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -31,23 +31,41 @@ check(Global, Local, #httpd{}=Req) global_config() -> % Return the globally-configured CORS settings in a format identical % to that in the _security object. - Enabled = couch_util:to_existing_atom( - couch_config:get("httpd", "cors_enabled", "false")), + % + % E.g., example.com allows origins http://origin.com and https://origin.com + % { "origins": + % { "example.com": + % { "http://origin.com": {"max_age":1234, "allow_methods":"GET"} + % , "https://origin.com": {"allow_methods": "PUT, POST"} + % } + % } + % } XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"), - DomainsToOrigins = binary_section("origins"), - Global = [ {<<"httpd">>, {[ {<<"cors_enabled">>, Enabled} - , {<<"x_forwarded_host">>, XHost} - ]}} - , {<<"origins">>, {DomainsToOrigins}} - ], - - AllOrigins = lists:flatten([ re:split(Line, ",\\s*") - || {_Vhost, Line} <- DomainsToOrigins]), - - lists:foldl(fun(OriginName, Config) -> - Stanza = {OriginName, binary_section(OriginName)}, - lists:keystore(OriginName, 1, Config, Stanza) - end, Global, AllOrigins). + Enabled = couch_config:get("httpd", "cors_enabled", "false"), + Origins = global_config(origins), + + [ {<<"httpd">>, + {[ {<<"cors_enabled">>, couch_util:to_existing_atom(Enabled)} + , {<<"x_forwarded_host">>, ?l2b(XHost)} + ]} } + , {<<"origins">>, {Origins}} + ]. + +global_config(origins) -> + % Return the .origins object of the config. Map each domain (vhost) to the + % origins it supports. Each supported origin is itself an object indicating + % allowed headers, max age, etc. + OriginsSection = couch_config:get("origins"), + lists:foldl(fun({Key, Val}, State) -> + Domain = ?l2b(Key), + Origins = re:split(Val, ",\\s*"), + DomainObj = lists:foldl(fun(Origin, DomainState) -> + Policy = binary_section(Origin), + lists:keystore(Origin, 1, DomainState, {Origin, {Policy}}) + end, [], Origins), + lists:keystore(Domain, 1, State, {Domain, {DomainObj}}) + end, [], OriginsSection). + binary_section(Section) -> % Return a config section, with all strings converted to binaries. diff --git a/test/etap/251-cors-config.t b/test/etap/251-cors-config.t index 5e8dbb65fb4..3b650944a5e 100755 --- a/test/etap/251-cors-config.t +++ b/test/etap/251-cors-config.t @@ -32,7 +32,7 @@ default_config() -> main(_) -> test_util:init_code_path(), - etap:plan(12), + etap:plan(14), case (catch test()) of ok -> etap:end_tests(); @@ -75,7 +75,7 @@ test_policy_structure() -> etap:ok(is_boolean(Enabled), "Boolean global CORS enabled flag"), XHost = couch_util:get_value(<<"x_forwarded_host">>, Httpd), - etap:is(XHost, "X-Forwarded-Host", + etap:is(XHost, <<"X-Forwarded-Host">>, "CORS config has X-Forwarded-Host setting"), OriginsObj = couch_util:get_value(<<"origins">>, Config), @@ -84,15 +84,21 @@ test_policy_structure() -> {Origins} = OriginsObj, etap:ok(is_list(Origins), "Global CORS origins section looks good"), - Example = couch_util:get_value(<<"example.com">>, Origins), - etap:is(Example, <<"http://origin.com, https://origin.com:6984">>, - "Global CORS origin: example.com"), + ExampleObj = couch_util:get_value(<<"example.com">>, Origins), + etap:ok(is_tuple(ExampleObj), "Configured origins object: example.com"), - Origin1 = couch_util:get_value(<<"http://origin.com">>, Config), - etap:ok(is_list(Origin1), "CORS origin stanza: http://origin.com"), + Nope1 = couch_util:get_value(<<"http://origin.com">>, Config, false), + etap:not_ok(Nope1, "No top-level config: http://origin.com"), - Origin2 = couch_util:get_value(<<"https://origin.com:6984">>, Config), - etap:ok(is_list(Origin2), "CORS origin stanza: https://origin.com:6984"), + Nope2 = couch_util:get_value(<<"http://origin.com">>, Config, false), + etap:not_ok(Nope2, "No top-level config: https://origin.com:6984"), + + {Example} = ExampleObj, + {Origin1} = couch_util:get_value(<<"http://origin.com">>, Example), + etap:ok(is_list(Origin1), "CORS origin config: http://origin.com"), + + {Origin2} = couch_util:get_value(<<"http://origin.com">>, Example), + etap:ok(is_list(Origin2), "CORS origin config: http://origin.com"), ok. % From 62582c262270c405ad8cef76cbf96ae38de46861 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Mon, 16 Jan 2012 12:56:05 +0000 Subject: [PATCH 08/29] Set default values into the CORS policies --- src/couchdb/couch_cors_policy.erl | 66 +++++++++++++++++++++++++++++++ test/etap/251-cors-config.t | 21 +++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index a55b8399116..f63ba81d4d9 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -13,8 +13,23 @@ -module(couch_cors_policy). -export([global_config/0, check/2, check/3]). +% For the test suite. +-export([origins_config/3]). + -include("couch_db.hrl"). +-define(DEFAULT_CORS_POLICY, + [ {<<"allow_credentials">>, false} + , {<<"max_age">>, 4 * 60 * 60} + , {<<"allow_methods">>, <<"GET, HEAD, POST">>} + , {<<"allow_headers">>, + <<"Content-Length, If-Match, Destination" + , ", X-HTTP-Method-Override" + , ", X-Requested-With" % For jQuery v1.5.1 + >>} + ]). + + check(DbConfig, #httpd{}=Req) -> check(global_config(), DbConfig, Req). @@ -28,6 +43,57 @@ check(Global, Local, #httpd{}=Req) end. +origins_config(Global, Local, Req) -> + % Identify the "origins" configuration object which applies to this + % request. The local (i.e. _security object) config takes precidence. + {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), + XHost = couch_util:get_value(<<"x_forwarded_host">>, Httpd, + "X-Forwarded-Host"), + VHost = case couch_httpd:header_value(Req, XHost) of + undefined -> + case couch_httpd:header_value(Req, "Host") of + undefined -> ""; + HostValue -> ?l2b(HostValue) + end; + ForwardedValue -> ?l2b(ForwardedValue) + end, + + {GlobalHosts} = couch_util:get_value(<<"origins">>, Global, {[]}), + {LocalHosts} = couch_util:get_value(<<"origins">>, Local, {[]}), + Origins = case couch_util:get_value(VHost, LocalHosts) of + {LocalObj} -> + ?LOG_DEBUG("Local origin list: ~s", [VHost]), + LocalObj; + _ -> + ?LOG_DEBUG("No local origin list: ~s", [VHost]), + case couch_util:get_value(VHost, GlobalHosts) of + {GlobalObj} -> + ?LOG_DEBUG("Global origin list: ~s", [VHost]), + GlobalObj; + _ -> + ?LOG_DEBUG("No global origin list: ~s", [VHost]), + [] + end + end, + + origins_config(Origins). + +origins_config(BaseOrigins) -> + % Normalize the config object for origins, apply defaults, etc. If no + % origins are specified, provide a default wildcard entry. + Defaulted = fun(Policy) -> + lists:foldl(fun({Key, Val}, State) -> + lists:keyreplace(Key, 1, State, {Key, Val}) + end, ?DEFAULT_CORS_POLICY, Policy) + end, + + Origins = case BaseOrigins of + [] -> [ {<<"*">>, {[]}} ]; + _ -> BaseOrigins + end, + + [ {Key, {Defaulted(Policy)}} || {Key, {Policy}} <- Origins ]. + global_config() -> % Return the globally-configured CORS settings in a format identical % to that in the _security object. diff --git a/test/etap/251-cors-config.t b/test/etap/251-cors-config.t index 3b650944a5e..8cfcdca6f6d 100755 --- a/test/etap/251-cors-config.t +++ b/test/etap/251-cors-config.t @@ -32,7 +32,7 @@ default_config() -> main(_) -> test_util:init_code_path(), - etap:plan(14), + etap:plan(18), case (catch test()) of ok -> etap:end_tests(); @@ -47,6 +47,7 @@ test() -> couch_config:start_link([default_config()]), test_api_calls(), test_policy_structure(), + test_defaults(), ok. test_api_calls() -> @@ -101,6 +102,24 @@ test_policy_structure() -> etap:ok(is_list(Origin2), "CORS origin config: http://origin.com"), ok. + +test_defaults() -> + Headers = mochiweb_headers:make([]), + MochiReq = mochiweb_request:new(nil, 'GET', "/", {1,1}, Headers), + Req = #httpd{mochi_req=MochiReq}, + + Hosts = couch_cors_policy:origins_config([], [], Req), + {Config} = couch_util:get_value(<<"*">>, Hosts), + + etap:ok(is_list(Config), "Default CORS origin is *"), + etap:is(couch_util:get_value(<<"allow_credentials">>, Config), + false, "CORS default: allow_credentials"), + etap:is(couch_util:get_value(<<"max_age">>, Config), + 14400, "CORS default: max_age"), + etap:is(couch_util:get_value(<<"allow_methods">>, Config), + <<"GET, HEAD, POST">>, "CORS default: allow_methods"), + ok. + % % Utilities % From cb37e1ec674dd69013f5ef0422683b0ae1ed2e38 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Mon, 16 Jan 2012 13:10:29 +0000 Subject: [PATCH 09/29] Implement "actual" cross-origin response (s. 5.1) --- src/couchdb/couch_cors_policy.erl | 112 +++++++++++++++++++++++++++++- test/etap/250-cors-policy.t | 91 +++++++++++++++++------- 2 files changed, 177 insertions(+), 26 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index f63ba81d4d9..10460c207be 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -38,11 +38,121 @@ check(Global, Local, #httpd{}=Req) {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), Enabled = couch_util:get_value(<<"cors_enabled">>, Httpd, false), case Enabled of - true -> []; + true -> + case couch_httpd:header_value(Req, "Origin") of + undefined -> + % s. 5.1(1) and s. 5.2(1) If the Origin header is not + % present terminate this set of steps. The request is + % outside the scope of this specification. + ?LOG_DEBUG("Not a CORS request", []), + false; + Origin -> + headers(Global, Local, Req, Origin) + end; _ -> false end. +headers(Global, Local, Req, OriginValue) -> + Policies = origins_config(Global, Local, Req), + + % s. 5 ...each resource is bound to the following: + % * A list of origins consisting of zero or more origins that are allowed + % to access this resource. + % * A list of methods consisting of zero or more methods that are + % supported by the resource. + % * A list of headers consisting of zero or more header field names that + % are supported by the resource. + % * A supports credentials flag that indicates whether the resource + % supports user credentials in the request. [true/false] + Origins = list_of_origins(Policies, Req), + Methods = list_of_methods(Policies, Req), + Headers = list_of_headers(Policies, Req), + Creds = supports_credentials(Policies, Req), + + case Req#httpd.method of + 'OPTIONS' -> + % s. 5.2. Preflight Request; also s. 6.1.5(1) + preflight(OriginValue, Origins, Methods, Headers, Creds, Req); + _ -> + % s. 5.1. Simple X-O Request, Actual Request, and Redirects. + actual(OriginValue, Origins, Methods, Headers, Creds, Req) + end. + +preflight(OriginVal, OkOrigins, OkMethods, OkHeaders, OkCreds, Req) -> + % Note that s. 5.1(2) (actual requests) requires splitting the Origin + % header, and AFAICT all tokens must match the list of origins. But + % s. 5.2.2 (preflight requests) implies that the Origin header will contain + % only one token. Assume that the "source origin" (s. 6.1) is the first in + % the list? + SourceOrigin = lists:nth(1, string:tokens(OriginVal, " ")), + false. + +actual(OriginVal, OkOrigins, _OkMethods, _OkHeaders, OkCreds, _Req) -> + % s. 5.1(2) Split the value of the Origin header on the U+0020 SPACE + % character and if any of the resulting tokens is not a case-sensitive + % match for any of the values in [OkOrigins] do not set any additional + % headers and terminate... + Origins = [ ?l2b(O) || O <- string:tokens(OriginVal, " ") ], + GoodOrigin = fun(Origin) -> + lists:any(fun(OkOrigin) -> + OkOrigin == <<"*">> orelse OkOrigin == Origin + end, OkOrigins) + end, + + case lists:all(GoodOrigin, Origins) of + false -> + ?LOG_DEBUG("Origin ~p not allowed: ~p", [Origins, OkOrigins]), + false; + true -> + % s. 5.1(3) If the resource supports credentials add a single + % A-C-A-Origin header, with the value of the origin header as + % value, and add a single A-C-A-Credentials header with the literal + % string "true" as value. + % + % Otherwise, add a single A-C-A-Origin header with either the value + % of the Origin header or the literal string "*" as value. + Allow = case OkCreds of + true -> [ {"Access-Control-Allow-Origin", OriginVal} + , {"Access-Control-Allow-Credentials", "true"} + ]; + false -> [ {"Access-Control-Allow-Origin", OriginVal} ] + end, + + % s. 5.1(4) If the resource wants to expose more than just simple + % response headers to the API of the CORS API specification add one + % or more Access-Control-Expose-Headers headers, with as values the + % filed names of the additional headers to expose. + CouchHeaders = [ "Server", "Date" + , "Content-Length" + , "ETag", "Age" + , "Connection" % ? + % Any others? + ], + + % TODO: Merged with the configured expose_headers? + Expose = [{"Access-Control-Expose-Headers", + string:join(CouchHeaders, ",")}], + + % Interestingly, the spec does not confirm policy for actual + % requests. This function ignores methods, headers, and the request + % object. Of course, disabling CORS on CouchDB, or removing an + % origin from the config would have immediate effect. Perhaps a + % future feature could detect minor changes to the CORS policy and + % purge the cache as mentioned in the 5.1(4) note. + Allow ++ Expose + end. + +list_of_origins(Config, _Req) -> + Keys = [ Key || {Key, _Val} <- Config ]. + +list_of_methods(Config, Req) -> + []. +list_of_headers(Config, Req) -> + []. +supports_credentials(Config, Req) -> + false. + origins_config(Global, Local, Req) -> % Identify the "origins" configuration object which applies to this % request. The local (i.e. _security object) config takes precidence. diff --git a/test/etap/250-cors-policy.t b/test/etap/250-cors-policy.t index e553c947181..1269db05d36 100755 --- a/test/etap/250-cors-policy.t +++ b/test/etap/250-cors-policy.t @@ -32,7 +32,7 @@ main(_) -> test_util:init_code_path(), - etap:plan(18), + etap:plan(26), case (catch test()) of ok -> etap:end_tests(); @@ -47,6 +47,7 @@ test() -> test_good_api_calls(), test_disabling(), test_enabled(), + test_default_policy(), test_duels(), ok. @@ -84,8 +85,8 @@ test_good_api_calls() -> test_disabling() -> Default = config(), Enabled = config([enabled]), % Enabled only, nothing else. - Config = config([enabled, {"example.com", "origin.com"}]), - Deactivated = config([{"example.com", "origin.com"}]), + Config = config([enabled, {"example.com", "http://origin.com"}]), + Deactivated = config([{"example.com", "http://origin.com"}]), etap:is(check(Default, Default, req()), false, "By default, CORS is disabled"), @@ -96,6 +97,11 @@ test_disabling() -> etap:is(check(Deactivated, Config, req()), false, "Deactivated CORS still overrides _security"), + etap:is(check(Config, [], httpd()), + false, "CORS fail for request with no Origin header"), + etap:is(check(Enabled, Config, httpd()), + false, "CORS fail from _security for request with no Origin"), + etap:isnt(check(Enabled, Config, req()), false, "Globally enabled CORS, config in _security: passes"), etap:isnt(check(Config, Default, req()), @@ -104,13 +110,45 @@ test_disabling() -> test_enabled() -> Enabled = config([enabled]), - Config = config([enabled, {"example.com","origin.com"}]), + Config = config([enabled, {"example.com","http://origin.com"}]), etap:ok(is_list(check(Enabled, Config, req())), "Good CORS from _security returns a list"), etap:ok(is_list(check(Config, [], req())), "Good CORS from _config returns a list"), ok. + +test_default_policy() -> + Enabled = config([enabled]), + Config = config([enabled, {"example.com","http://origin.com"}]), + + HeaderIs = fun(Global, Local, Req, Suffix, Expected, Description) -> + Result = couch_cors_policy:check(Global, Local, Req), + etap:ok(is_list(Result), "List returned: " ++ Description), + + % Protect against crashing in case the result was not a list. + case is_list(Result) of + false -> + etap:ok(false, "No headers either: " ++ Description); + true -> + Key = "Access-Control-" ++ Suffix, + Value = case lists:keyfind(Key, 1, Result) of + {Key, Val} -> Val; + _ -> undefined + end, + etap:is(Value, Expected, Description) + end + end, + + HeaderIs(Enabled, [], req(), "Allow-Origin", + "http://origin.com", "Default CORS policy echoes the header"), + HeaderIs(Enabled, Config, req(), "Allow-Origin", + "http://origin.com", "Satisfied CORS policy echoes the header"), + + HeaderIs(Enabled, [], req(), "Allow-Methods", + undefined, "Actual response does not send preflight headers"), + ok. + test_duels() -> % Configs from _security and _config have a duel! %TODO @@ -141,7 +179,13 @@ httpd(Path) -> httpd(Method, Path) -> Parts = [ list_to_binary(Part) || Part <- string:tokens(Path, "/") ], - #httpd{method=Method, requested_path_parts=Parts, path_parts=Parts}. + Headers = mochiweb_headers:make([{"Stuff","I am stuff"}]), + MochiReq = mochiweb_request:new(nil, Method, Path, {1,1}, Headers), + #httpd{ method = Method + , mochi_req = MochiReq + , path_parts = Parts + , requested_path_parts = Parts + }. req() -> req(httpd()). @@ -154,14 +198,14 @@ req(#httpd{method=Method, path_parts=Parts}=Req, Origin) -> Method = Req#httpd.method, Path = filename:join(Parts), Version = {1,1}, - Headers = mochiweb_headers:make([{"Origin", Origin}]), + Headers = mochiweb_headers:make([{"Origin", Origin}, {"Host","example.com"}]), MochiReq = mochiweb_request:new(nil, Method, Path, Version, Headers), Req#httpd{ mochi_req=MochiReq }. % Example, CORS enabled, mydomain.com allows http://origin.com with a max-age: % config([enabled, % {"mydomain.com","http://origin.com"}, -% ["http://origin.com", {"max-age",3600}]]) +% {"mydomain.com","https://origin.com", [{"allow_credentials",true}]} ]) config() -> config([]). config(Opts) -> @@ -175,29 +219,26 @@ config([enabled | Opts], Config) -> config(Opts, Config1); config([ {Dom, Orig} | Opts ], Config) -> + config([ {Dom, Orig, []} | Opts ], Config); + +config([ {Dom, Orig, Policy} | Opts ], Config) -> Domain = list_to_binary(Dom), Origin = list_to_binary(Orig), - {Origins} = case lists:keyfind(<<"origins">>, 1, Config) of - {<<"origins">>, FoundOrigins} -> FoundOrigins; + + {Domains} = case lists:keyfind(<<"origins">>, 1, Config) of + {<<"origins">>, FoundDomains} -> FoundDomains; + false -> {[]} + end, + {Origins} = case lists:keyfind(Domain, 1, Domains) of + {Domain, FoundOrigins} -> FoundOrigins; false -> {[]} end, - Origins1 = lists:keystore(Domain, 1, Origins, {Domain, Origin}), - Config1 = lists:keystore(<<"origins">>, 1, Config, {<<"origins">>, {Origins1}}), - config(Opts, Config1); -config([ [Orig|KVs] | Opts ], Config) -> - Origin = list_to_binary(Orig), - Configs = lists:foldl(fun({KeyStr, ValStr}, OriginCfg) -> - Key = list_to_binary(KeyStr), - Val = case ValStr of - "true" -> true; - "false" -> false; - Num when is_number(Num) -> Num; - _ -> list_to_binary(ValStr) - end, - lists:keystore(Key, 1, OriginCfg, {Key, Val}) - end, [], KVs), - Config1 = lists:keystore(Origin, 1, Config, {Origin, {Configs}}), + Policy1 = [{list_to_binary(Key), Val} || {Key, Val} <- Policy], + Origins1 = lists:keystore(Origin, 1, Origins, {Origin, {Policy1}}), + Domains1 = lists:keystore(Domain, 1, Domains, {Domain, {Origins1}}), + Config1 = lists:keystore(<<"origins">>, 1, Config, + {<<"origins">>, {Domains1}}), config(Opts, Config1). % vim: sts=4 sw=4 et From cf3f9c8c63b3fa38469d5e2bafb7951424d5a753 Mon Sep 17 00:00:00 2001 From: "Jason Smith (air)" Date: Sat, 18 Feb 2012 03:04:52 +0000 Subject: [PATCH 10/29] CORS policy returns an empty list instead of false for bad matches --- src/couchdb/couch_cors_policy.erl | 9 +++++---- test/etap/250-cors-policy.t | 16 ++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index 10460c207be..04c9aa6231d 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -45,11 +45,12 @@ check(Global, Local, #httpd{}=Req) % present terminate this set of steps. The request is % outside the scope of this specification. ?LOG_DEBUG("Not a CORS request", []), - false; + []; Origin -> headers(Global, Local, Req, Origin) end; - _ -> false + _ -> + [] end. @@ -86,7 +87,7 @@ preflight(OriginVal, OkOrigins, OkMethods, OkHeaders, OkCreds, Req) -> % only one token. Assume that the "source origin" (s. 6.1) is the first in % the list? SourceOrigin = lists:nth(1, string:tokens(OriginVal, " ")), - false. + []. actual(OriginVal, OkOrigins, _OkMethods, _OkHeaders, OkCreds, _Req) -> % s. 5.1(2) Split the value of the Origin header on the U+0020 SPACE @@ -103,7 +104,7 @@ actual(OriginVal, OkOrigins, _OkMethods, _OkHeaders, OkCreds, _Req) -> case lists:all(GoodOrigin, Origins) of false -> ?LOG_DEBUG("Origin ~p not allowed: ~p", [Origins, OkOrigins]), - false; + []; true -> % s. 5.1(3) If the resource supports credentials add a single % A-C-A-Origin header, with the value of the origin header as diff --git a/test/etap/250-cors-policy.t b/test/etap/250-cors-policy.t index 1269db05d36..5a6b70ba553 100755 --- a/test/etap/250-cors-policy.t +++ b/test/etap/250-cors-policy.t @@ -89,23 +89,23 @@ test_disabling() -> Deactivated = config([{"example.com", "http://origin.com"}]), etap:is(check(Default, Default, req()), - false, "By default, CORS is disabled"), + [], "By default, CORS is disabled"), etap:is(check(Default, Config, req()), - false, "By default, CORS is disabled, despite _security"), + [], "By default, CORS is disabled, despite _security"), etap:is(check(Deactivated, Default, req()), - false, "Globally disabling CORS overrides everything else"), + [], "Globally disabling CORS overrides everything else"), etap:is(check(Deactivated, Config, req()), - false, "Deactivated CORS still overrides _security"), + [], "Deactivated CORS still overrides _security"), etap:is(check(Config, [], httpd()), - false, "CORS fail for request with no Origin header"), + [], "CORS fail for request with no Origin header"), etap:is(check(Enabled, Config, httpd()), - false, "CORS fail from _security for request with no Origin"), + [], "CORS fail from _security for request with no Origin"), etap:isnt(check(Enabled, Config, req()), - false, "Globally enabled CORS, config in _security: passes"), + [], "Globally enabled CORS, config in _security: passes"), etap:isnt(check(Config, Default, req()), - false, "Global CORS config, nothing in _security: passes"), + [], "Global CORS config, nothing in _security: passes"), ok. test_enabled() -> From 7bff399fe225d346dc4dd423cbebd432c7b62701 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sat, 18 Feb 2012 09:58:09 +0000 Subject: [PATCH 11/29] Purge HTTP knowledge from the cors policy; the method and headers proplist are parameters --- src/couchdb/couch_cors_policy.erl | 31 +++++++++--------- test/etap/250-cors-policy.t | 52 +++++++++---------------------- test/etap/251-cors-config.t | 22 ++----------- 3 files changed, 34 insertions(+), 71 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index 04c9aa6231d..68990eeedb8 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -30,23 +30,24 @@ ]). -check(DbConfig, #httpd{}=Req) -> +check(DbConfig, Req) -> check(global_config(), DbConfig, Req). -check(Global, Local, #httpd{}=Req) - when is_list(Global) andalso is_list(Local) -> +check(Global, Local, {Method, Headers}=Req) + when is_list(Global) andalso is_list(Local) andalso is_list(Headers) + andalso (is_atom(Method) orelse is_binary(Method)) -> {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), Enabled = couch_util:get_value(<<"cors_enabled">>, Httpd, false), case Enabled of true -> - case couch_httpd:header_value(Req, "Origin") of - undefined -> + case lists:keyfind("Origin", 1, Headers) of + false -> % s. 5.1(1) and s. 5.2(1) If the Origin header is not % present terminate this set of steps. The request is % outside the scope of this specification. ?LOG_DEBUG("Not a CORS request", []), []; - Origin -> + {"Origin", Origin} -> headers(Global, Local, Req, Origin) end; _ -> @@ -54,7 +55,7 @@ check(Global, Local, #httpd{}=Req) end. -headers(Global, Local, Req, OriginValue) -> +headers(Global, Local, {ReqMethod, _ReqHeaders}=Req, OriginValue) -> Policies = origins_config(Global, Local, Req), % s. 5 ...each resource is bound to the following: @@ -71,7 +72,7 @@ headers(Global, Local, Req, OriginValue) -> Headers = list_of_headers(Policies, Req), Creds = supports_credentials(Policies, Req), - case Req#httpd.method of + case ReqMethod of 'OPTIONS' -> % s. 5.2. Preflight Request; also s. 6.1.5(1) preflight(OriginValue, Origins, Methods, Headers, Creds, Req); @@ -154,19 +155,19 @@ list_of_headers(Config, Req) -> supports_credentials(Config, Req) -> false. -origins_config(Global, Local, Req) -> +origins_config(Global, Local, {Method, Headers}) -> % Identify the "origins" configuration object which applies to this % request. The local (i.e. _security object) config takes precidence. {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), XHost = couch_util:get_value(<<"x_forwarded_host">>, Httpd, "X-Forwarded-Host"), - VHost = case couch_httpd:header_value(Req, XHost) of - undefined -> - case couch_httpd:header_value(Req, "Host") of - undefined -> ""; - HostValue -> ?l2b(HostValue) + VHost = case lists:keyfind(XHost, 1, Headers) of + false -> + case lists:keyfind("Host", 1, Headers) of + false -> ""; + {"Host", HostValue} -> ?l2b(HostValue) end; - ForwardedValue -> ?l2b(ForwardedValue) + {XHost, ForwardedValue} -> ?l2b(ForwardedValue) end, {GlobalHosts} = couch_util:get_value(<<"origins">>, Global, {[]}), diff --git a/test/etap/250-cors-policy.t b/test/etap/250-cors-policy.t index 5a6b70ba553..0222c790f1e 100755 --- a/test/etap/250-cors-policy.t +++ b/test/etap/250-cors-policy.t @@ -14,25 +14,10 @@ % Test only the policy decisions of couch_cors_policy:check/3--no servers % or other couch_* stuff. --record(httpd, - {mochi_req, - peer, - method, - requested_path_parts, - path_parts, - db_url_handlers, - user_ctx, - req_body = undefined, - design_url_handlers, - auth, - default_fun, - url_handlers - }). - main(_) -> test_util:init_code_path(), - etap:plan(26), + etap:plan(27), case (catch test()) of ok -> etap:end_tests(); @@ -59,20 +44,22 @@ test_bad_api_calls() -> etap_threw(fun() -> couch_cors_policy:check([], []) end, true, "Policy check with two parameters"), - etap_threw(fun() -> couch_cors_policy:check(notList, [], #httpd{}) end, + etap_threw(fun() -> couch_cors_policy:check(notList, [], {'GET', []}) end, true, "Policy check with non-list first parameter"), - etap_threw(fun() -> couch_cors_policy:check([], notList, #httpd{}) end, + etap_threw(fun() -> couch_cors_policy:check([], notList, {'GET', []}) end, true, "Policy check with non-list second parameter"), - etap_threw(fun() -> couch_cors_policy:check([], [], {not_httpd}) end, - true, "Policy check with non-#httpd{} third parameter"), + etap_threw(fun() -> couch_cors_policy:check([], [], {{not_method}, []}) end, + true, "Policy check with non-method third parameter"), + etap_threw(fun() -> couch_cors_policy:check([], [], {'GET', not_list}) end, + true, "Policy check with non-list fourth parameter"), ok. test_good_api_calls() -> - etap_threw(fun() -> couch_cors_policy:check([], [], #httpd{}) end, + etap_threw(fun() -> couch_cors_policy:check([], [], {'GET', []}) end, false, "Policy check with three valid parameters"), etap_threw(fun() -> - couch_cors_policy:check(config(), config(), #httpd{}) + couch_cors_policy:check(config(), config(), {'GET', []}) end, false, "Policy check with noop configs"), % And the shortcut function. @@ -178,14 +165,8 @@ httpd(Path) -> httpd('GET', Path). httpd(Method, Path) -> - Parts = [ list_to_binary(Part) || Part <- string:tokens(Path, "/") ], - Headers = mochiweb_headers:make([{"Stuff","I am stuff"}]), - MochiReq = mochiweb_request:new(nil, Method, Path, {1,1}, Headers), - #httpd{ method = Method - , mochi_req = MochiReq - , path_parts = Parts - , requested_path_parts = Parts - }. + Headers = [{"Stuff","I am stuff"}], + {Method, Headers}. req() -> req(httpd()). @@ -193,14 +174,11 @@ req(Origin) when is_list(Origin) -> req(httpd(), Origin); req(Httpd) -> req(Httpd, "http://origin.com"). -req(#httpd{method=Method, path_parts=Parts}=Req, Origin) -> +req({Method, Headers}, Origin) -> % Give this request the Origin. - Method = Req#httpd.method, - Path = filename:join(Parts), - Version = {1,1}, - Headers = mochiweb_headers:make([{"Origin", Origin}, {"Host","example.com"}]), - MochiReq = mochiweb_request:new(nil, Method, Path, Version, Headers), - Req#httpd{ mochi_req=MochiReq }. + OriginHeaders = lists:keystore("Origin", 1, Headers, {"Origin", Origin}), + HostHeaders = lists:keystore("Host", 1, OriginHeaders, {"Host", "example.com"}), + {Method, HostHeaders}. % Example, CORS enabled, mydomain.com allows http://origin.com with a max-age: % config([enabled, diff --git a/test/etap/251-cors-config.t b/test/etap/251-cors-config.t index 8cfcdca6f6d..ae5cbf345de 100755 --- a/test/etap/251-cors-config.t +++ b/test/etap/251-cors-config.t @@ -11,21 +11,6 @@ % License for the specific language governing permissions and limitations under % the License. --record(httpd, - {mochi_req, - peer, - method, - requested_path_parts, - path_parts, - db_url_handlers, - user_ctx, - req_body = undefined, - design_url_handlers, - auth, - default_fun, - url_handlers - }). - default_config() -> test_util:build_file("etc/couchdb/default_dev.ini"). @@ -55,7 +40,7 @@ test_api_calls() -> false, "No problem building global CORS policy"), % Test the "shortcut" policy check call, which requires couch_config. - etap:not_ok(threw(fun() -> couch_cors_policy:check([], #httpd{}) end), + etap:not_ok(threw(fun() -> couch_cors_policy:check([], {'GET', []}) end), "Policy check with two valid parameters"), ok. @@ -104,9 +89,8 @@ test_policy_structure() -> ok. test_defaults() -> - Headers = mochiweb_headers:make([]), - MochiReq = mochiweb_request:new(nil, 'GET', "/", {1,1}, Headers), - Req = #httpd{mochi_req=MochiReq}, + Headers = [], + Req = {'GET', Headers}, Hosts = couch_cors_policy:origins_config([], [], Req), {Config} = couch_util:get_value(<<"*">>, Hosts), From 5f1b506298350d8e603c883a5113c0e6a36633e1 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sat, 18 Feb 2012 10:04:13 +0000 Subject: [PATCH 12/29] Run the tests in easiest-to-learn order --- test/etap/{251-cors-config.t => 250-cors-config.t} | 0 test/etap/{250-cors-policy.t => 251-cors-policy.t} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/etap/{251-cors-config.t => 250-cors-config.t} (100%) rename test/etap/{250-cors-policy.t => 251-cors-policy.t} (100%) diff --git a/test/etap/251-cors-config.t b/test/etap/250-cors-config.t similarity index 100% rename from test/etap/251-cors-config.t rename to test/etap/250-cors-config.t diff --git a/test/etap/250-cors-policy.t b/test/etap/251-cors-policy.t similarity index 100% rename from test/etap/250-cors-policy.t rename to test/etap/251-cors-policy.t From e0fdba3c6939916d27d5b3d1beb608b155c69ad5 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sat, 18 Feb 2012 10:13:50 +0000 Subject: [PATCH 13/29] Better confirmation that enabled sites work --- test/etap/251-cors-policy.t | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/etap/251-cors-policy.t b/test/etap/251-cors-policy.t index 0222c790f1e..c73be3aaf2b 100755 --- a/test/etap/251-cors-policy.t +++ b/test/etap/251-cors-policy.t @@ -17,7 +17,7 @@ main(_) -> test_util:init_code_path(), - etap:plan(27), + etap:plan(29), case (catch test()) of ok -> etap:end_tests(); @@ -98,10 +98,15 @@ test_disabling() -> test_enabled() -> Enabled = config([enabled]), Config = config([enabled, {"example.com","http://origin.com"}]), - etap:ok(is_list(check(Enabled, Config, req())), - "Good CORS from _security returns a list"), - etap:ok(is_list(check(Config, [], req())), - "Good CORS from _config returns a list"), + + FromSecurity = check(Enabled, Config, req()), + etap:ok(is_list(FromSecurity), "Header list from _security"), + etap:isnt(FromSecurity, [], "Header list from _security is non-empty"), + + FromConfig = check(Config, [], req()), + etap:ok(is_list(FromConfig), "Header list from _config"), + etap:isnt(FromConfig, [], "Header list from _config is non-empty"), + ok. From 35139fd293a7fa2fd8454504589ae2483117493f Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sat, 18 Feb 2012 10:28:07 +0000 Subject: [PATCH 14/29] Import tests and code from Benoit's patch in COUCHDB-431 --- src/couchdb/couch_httpd_cors.erl | 178 ++++++++++++++++++++++ test/etap/252-cors-functionality.t | 234 +++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+) create mode 100644 src/couchdb/couch_httpd_cors.erl create mode 100644 test/etap/252-cors-functionality.t diff --git a/src/couchdb/couch_httpd_cors.erl b/src/couchdb/couch_httpd_cors.erl new file mode 100644 index 00000000000..e58a2152753 --- /dev/null +++ b/src/couchdb/couch_httpd_cors.erl @@ -0,0 +1,178 @@ +% 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. + +%% the cors utilities +%% +%% CORS processing is done by adding CORS headers to the cors_headers +%% variable in the process registry. Then headers are added to the final +%% response headers while processing the response using the cors_headers +%% function. +%% +%% Process: +%% 1. set default cors headers using set_default_headers when couchdb +%% start to process the request +%% 2. on OPTION method, check asked capabilities and eventually return +%% preflight headers. At this steps CORS headers can be [] (capability not +%% handled) or the list of preflight headers like the spec require. +%% preflight headers are set in preflight_headers method. +%% 3. on database request , we check origin header in a list of origin. +%% Eventually we reset cors headers to [] if the origin isn't +%% supported. Db origins are added to teh "origin" member of the +%% security object. db_check_origin function is used to check them. + +-module(couch_httpd_cors). + +-include("couch_db.hrl"). + +-define(SUPPORTED_HEADERS, [ + %% simple headers + "Accept", + "Accept-Language", + "Content-Type", + "Expires", + "Last-Modified", + "Pragma", + "Origin", + %% couchdb headers + "Content-Length", + "If-Match", + "Destination", + "X-Requested-With", + "X-Http-Method-Override", + "Content-Range"]). + +-export([set_default_headers/1, headers/0, + preflight_headers/1, preflight_headers/2, + db_check_origin/2]). + +set_default_headers(MochiReq) -> + case MochiReq:get_header_value("Origin") of + undefined -> + erlang:put(cors_headers, []); + Origin -> + DefaultHeaders = [{"Access-Control-Allow-Origin", Origin}, + {"Access-Control-Allow-Credentials", "true"}], + erlang:put(cors_headers, DefaultHeaders) + end. + +headers() -> + erlang:get(cors_headers). + +split_origin(Origin) -> + {Scheme, Netloc, _, _, _} = mochiweb_util:urlsplit(Origin), + {string:to_lower(Scheme), string:to_lower(Netloc)}. + +preflight_headers(MochiReq) -> + preflight_headers(MochiReq, [<<"*">>]). + +preflight_headers(#httpd{mochi_req=MochiReq}, AcceptedOrigins) -> + preflight_headers(MochiReq, AcceptedOrigins); +preflight_headers(MochiReq, AcceptedOrigins) -> + SupportedMethods = ["GET", "HEAD", "POST", "PUT", + "DELETE", "TRACE", "CONNECT", "COPY", "OPTIONS"], + + %% get custom headers + CustomHeaders = re:split(couch_config:get("cors", + "headers",""), "\\s*,\\s*",[{return, list}]), + + %% build list of headers to test + AllSupportedHeaders = ?SUPPORTED_HEADERS ++ CustomHeaders, + SupportedHeaders = [string:to_lower(H) || H <- AllSupportedHeaders], + + %% get max age + MaxAge = list_to_integer( + couch_config:get("cors", "max_age", "1000") + ), + + %% reset cors_headers + erlang:put(cors_headers, []), + + case MochiReq:get_header_value("Origin") of + undefined -> ok; + Origin -> + %% if origin validate it against accepted origin + case check_origin(AcceptedOrigins, split_origin(Origin)) of + error -> ok; %% don't set any preflight header + _Origin1 -> + ?LOG_DEBUG("check preflight cors request", []), + + PreflightHeaders0 = [ + {"Access-Control-Allow-Origin", Origin}, + {"Access-Control-Allow-Credentials", "true"}, + {"Access-Control-Max-Age", MaxAge}, + {"Access-Control-Allow-Methods", string:join(SupportedMethods, ", ")} + ], + + %% now check the reqquested method + case MochiReq:get_header_value("Access-Control-Request-Method") of + undefined -> + erlang:put(cors_headers, PreflightHeaders0); + Method -> + case lists:member(Method, SupportedMethods) of + true -> + %% method ok , check headers + {FinalReqHeaders, ReqHeaders} = case MochiReq:get_header_value( + "Access-Control-Request-Headers") of + undefined -> {"", []}; + Headers -> + %% transform header list in something we + %% could check. make sure everything is a + %% list + + RH = [string:to_lower(H) || H <- re:split(Headers, ",\\s*", + [{return,list},trim])], + {Headers, RH} + end, + + %% check if headers are supported + case ReqHeaders -- SupportedHeaders of + [] -> + PreflightHeaders = PreflightHeaders0 ++ + [{"Access-Control-Allow-Headers", FinalReqHeaders}], + erlang:put(cors_headers, PreflightHeaders); + _ -> ok + end; + false -> ok + end + end + end + end. + +check_origin([], _SO) -> + error; +check_origin([<<"*">>|_], _SO) -> + "*"; +check_origin([A0|R], SO) -> + A = couch_util:to_list(A0), + SA = split_origin(A), + + if SO == SA -> A; + true ->check_origin(R, SO) + end. + +db_check_origin(#httpd{mochi_req=MochiReq}, Db) -> + {SecProps} = couch_db:get_security(Db), + AcceptedOrigins = couch_util:get_value(<<"origins">>, SecProps, [<<"*">>]), + case MochiReq:get_header_value("Origin") of + undefined -> ok; + Origin -> + case check_origin(AcceptedOrigins, split_origin(Origin)) of + error -> + %% reset cors_headers + erlang:put(cors_headers, []); + _Origin1 -> + CorsHeaders = [{"Access-Control-Allow-Origin", Origin}, + {"Access-Control-Allow-Credentials", "true"}], + erlang:put(cors_headers, CorsHeaders) + end + end, + ok. diff --git a/test/etap/252-cors-functionality.t b/test/etap/252-cors-functionality.t new file mode 100644 index 00000000000..d03486f2d80 --- /dev/null +++ b/test/etap/252-cors-functionality.t @@ -0,0 +1,234 @@ +#!/usr/bin/env escript +% 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. + +-record(user_ctx, { + name = null, + roles = [], + handler +}). + + +-define(SUPPORTED_METHODS, "GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, COPY, OPTIONS"). +server() -> + lists:concat([ + "http://127.0.0.1:", + mochiweb_socket_server:get(couch_httpd, port), + "/" + ]). + + +main(_) -> + test_util:init_code_path(), + + etap:plan(9), + case (catch test()) of + ok -> + etap:end_tests(); + Other -> + etap:diag(io_lib:format("Test died abnormally: ~p", [Other])), + etap:bail(Other) + end, + ok. + +dbname() -> "etap-test-db". +dbname1() -> "etap-test-db1". +dbname2() -> "etap-test-db2". + +admin_user_ctx() -> {user_ctx, #user_ctx{roles=[<<"_admin">>]}}. + +set_admin_password(UserName, Password) -> + Salt = binary_to_list(couch_uuids:random()), + Hashed = couch_util:to_hex(crypto:sha(Password ++ Salt)), + couch_config:set("admins", UserName, + "-hashed-" ++ Hashed ++ "," ++ Salt, false). + + +secobj() -> + {[ + {<<"readers">>, {[{<<"names">>, []}, {<<"roles">>, []}]}}, + {<<"admins">>, {[{<<"names">>, []}, {<<"roles">>, []}]}}, + {<<"origins">>, [<<"http://example.com">>]} + ]}. + +secobj2() -> + {[ + {<<"readers">>, {[{<<"names">>, [<<"test">>]}, {<<"roles">>, []}]}}, + {<<"admins">>, {[{<<"names">>, []}, {<<"roles">>, []}]}} + ]}. + + +test() -> + %% launch couchdb + couch_server_sup:start_link(test_util:config_files()), + ibrowse:start(), + crypto:start(), + + %% initialize db + timer:sleep(1000), + couch_server:delete(list_to_binary(dbname()), [admin_user_ctx()]), + couch_server:delete(list_to_binary(dbname1()), [admin_user_ctx()]), + couch_server:delete(list_to_binary(dbname2()), [admin_user_ctx()]), + {ok, Db} = couch_db:create(list_to_binary(dbname()), [admin_user_ctx()]), + {ok, Db1} = couch_db:create(list_to_binary(dbname1()), [admin_user_ctx()]), + {ok, Db2} = couch_db:create(list_to_binary(dbname2()), [admin_user_ctx()]), + + ok = couch_db:set_security(Db1, secobj()), + ok = couch_db:set_security(Db2, secobj2()), + + %% do tests + test_simple_request(), + test_preflight_request(), + test_db_request(), + test_db_preflight_request(), + test_db_origin_request(), + test_db1_origin_request(), + test_db1_wrong_origin_request(), + + %% do tests with auth + ok = set_admin_password("test", "test"), + + test_db_preflight_auth_request(), + test_db_origin_auth_request(), + + %% restart boilerplate + catch couch_db:close(Db), + catch couch_db:close(Db1), + catch couch_db:close(Db2), + + couch_server:delete(list_to_binary(dbname()), [admin_user_ctx()]), + couch_server:delete(list_to_binary(dbname1()), [admin_user_ctx()]), + couch_server:delete(list_to_binary(dbname2()), [admin_user_ctx()]), + + + timer:sleep(3000), + couch_server_sup:stop(), + ok. + +test_simple_request() -> + Headers = [{"Origin", "http://127.0.0.1"}], + case ibrowse:send_req(server(), Headers, get, []) of + {ok, _, RespHeaders, _} -> + etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders), + "http://127.0.0.1", + "Access-Control-Allow-Origin ok"); + _ -> + etap:is(false, true, "ibrowse failed") + end. + +test_preflight_request() -> + Headers = [{"Origin", "http://127.0.0.1"}, + {"Access-Control-Request-Method", "GET"}], + case ibrowse:send_req(server(), Headers, options, []) of + {ok, _, RespHeaders, _} -> + etap:is(proplists:get_value("Access-Control-Allow-Methods", RespHeaders), + ?SUPPORTED_METHODS, + "Access-Control-Allow-Methods ok"); + _ -> + etap:is(false, true, "ibrowse failed") + end. + +test_db_request() -> + Headers = [{"Origin", "http://127.0.0.1"}], + Url = server() ++ "etap-test-db", + case ibrowse:send_req(Url, Headers, get, []) of + {ok, _, RespHeaders, _Body} -> + etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders), + "http://127.0.0.1", + "db Access-Control-Allow-Origin ok"); + _ -> + etap:is(false, true, "ibrowse failed") + end. + +test_db_preflight_request() -> + Url = server() ++ "etap-test-db", + Headers = [{"Origin", "http://127.0.0.1"}, + {"Access-Control-Request-Method", "GET"}], + case ibrowse:send_req(Url, Headers, options, []) of + {ok, _, RespHeaders, _} -> + etap:is(proplists:get_value("Access-Control-Allow-Methods", RespHeaders), + ?SUPPORTED_METHODS, + "db Access-Control-Allow-Methods ok"); + _ -> + etap:is(false, true, "ibrowse failed") + end. + + +test_db_origin_request() -> + Headers = [{"Origin", "http://127.0.0.1"}], + Url = server() ++ "etap-test-db", + case ibrowse:send_req(Url, Headers, get, []) of + {ok, _, RespHeaders, _Body} -> + etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders), + "http://127.0.0.1", + "db origin ok"); + _ -> + etap:is(false, true, "ibrowse failed") + end. + +test_db1_origin_request() -> + Headers = [{"Origin", "http://example.com"}], + Url = server() ++ "etap-test-db1", + case ibrowse:send_req(Url, Headers, get, [], [{host_header, "example.com"}]) of + {ok, _, RespHeaders, _Body} -> + etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders), + "http://example.com", + "db origin ok"); + _Else -> + io:format("else ~p~n", [_Else]), + etap:is(false, true, "ibrowse failed") + end. + +test_db1_wrong_origin_request() -> + Headers = [{"Origin", "http://localhost"}], + Url = server() ++ "etap-test-db1", + case ibrowse:send_req(Url, Headers, get, []) of + {ok, _, RespHeaders, _Body} -> + etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders), + undefined, + "db origin ok"); + _ -> + etap:is(false, true, "ibrowse failed") + end. + + +test_db_preflight_auth_request() -> + Url = server() ++ "etap-test-db2", + Headers = [{"Origin", "http://127.0.0.1"}, + {"Access-Control-Request-Method", "GET"}], + case ibrowse:send_req(Url, Headers, options, []) of + {ok, _Status, RespHeaders, _} -> + io:format("resp status ~p~n", [_Status]), + + io:format("resp headers ~p~n", [RespHeaders]), + etap:is(proplists:get_value("Access-Control-Allow-Methods", RespHeaders), + ?SUPPORTED_METHODS, + "db Access-Control-Allow-Methods ok"); + _ -> + etap:is(false, true, "ibrowse failed") + end. + + +test_db_origin_auth_request() -> + Headers = [{"Origin", "http://127.0.0.1"}], + Url = server() ++ "etap-test-db2", + + case ibrowse:send_req(Url, Headers, get, [], + [{basic_auth, {"test", "test"}}]) of + {ok, _, RespHeaders, _Body} -> + etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders), + "http://127.0.0.1", + "db origin ok"); + _ -> + etap:is(false, true, "ibrowse failed") + end. + From 322738a33ffa274bee16ed37633b636661cd6709 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sat, 18 Feb 2012 10:30:02 +0000 Subject: [PATCH 15/29] Rearrange and whitespace fix, for readability --- src/couchdb/couch_httpd_cors.erl | 34 ++++++------ test/etap/252-cors-functionality.t | 85 ++++++++++++++++-------------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/src/couchdb/couch_httpd_cors.erl b/src/couchdb/couch_httpd_cors.erl index e58a2152753..6d83bbd16fb 100644 --- a/src/couchdb/couch_httpd_cors.erl +++ b/src/couchdb/couch_httpd_cors.erl @@ -10,7 +10,7 @@ % License for the specific language governing permissions and limitations under % the License. -%% the cors utilities +%% the cors utilities %% %% CORS processing is done by adding CORS headers to the cors_headers %% variable in the process registry. Then headers are added to the final @@ -24,7 +24,7 @@ %% preflight headers. At this steps CORS headers can be [] (capability not %% handled) or the list of preflight headers like the spec require. %% preflight headers are set in preflight_headers method. -%% 3. on database request , we check origin header in a list of origin. +%% 3. on database request , we check origin header in a list of origin. %% Eventually we reset cors headers to [] if the origin isn't %% supported. Db origins are added to teh "origin" member of the %% security object. db_check_origin function is used to check them. @@ -35,19 +35,19 @@ -define(SUPPORTED_HEADERS, [ %% simple headers - "Accept", - "Accept-Language", + "Accept", + "Accept-Language", "Content-Type", - "Expires", - "Last-Modified", - "Pragma", + "Expires", + "Last-Modified", + "Pragma", "Origin", %% couchdb headers - "Content-Length", - "If-Match", + "Content-Length", + "If-Match", "Destination", - "X-Requested-With", - "X-Http-Method-Override", + "X-Requested-With", + "X-Http-Method-Override", "Content-Range"]). -export([set_default_headers/1, headers/0, @@ -56,7 +56,7 @@ set_default_headers(MochiReq) -> case MochiReq:get_header_value("Origin") of - undefined -> + undefined -> erlang:put(cors_headers, []); Origin -> DefaultHeaders = [{"Access-Control-Allow-Origin", Origin}, @@ -87,7 +87,7 @@ preflight_headers(MochiReq, AcceptedOrigins) -> %% build list of headers to test AllSupportedHeaders = ?SUPPORTED_HEADERS ++ CustomHeaders, SupportedHeaders = [string:to_lower(H) || H <- AllSupportedHeaders], - + %% get max age MaxAge = list_to_integer( couch_config:get("cors", "max_age", "1000") @@ -103,7 +103,7 @@ preflight_headers(MochiReq, AcceptedOrigins) -> case check_origin(AcceptedOrigins, split_origin(Origin)) of error -> ok; %% don't set any preflight header _Origin1 -> - ?LOG_DEBUG("check preflight cors request", []), + ?LOG_DEBUG("check preflight cors request", []), PreflightHeaders0 = [ {"Access-Control-Allow-Origin", Origin}, @@ -126,7 +126,7 @@ preflight_headers(MochiReq, AcceptedOrigins) -> Headers -> %% transform header list in something we %% could check. make sure everything is a - %% list + %% list RH = [string:to_lower(H) || H <- re:split(Headers, ",\\s*", [{return,list},trim])], @@ -136,7 +136,7 @@ preflight_headers(MochiReq, AcceptedOrigins) -> %% check if headers are supported case ReqHeaders -- SupportedHeaders of [] -> - PreflightHeaders = PreflightHeaders0 ++ + PreflightHeaders = PreflightHeaders0 ++ [{"Access-Control-Allow-Headers", FinalReqHeaders}], erlang:put(cors_headers, PreflightHeaders); _ -> ok @@ -154,7 +154,7 @@ check_origin([<<"*">>|_], _SO) -> check_origin([A0|R], SO) -> A = couch_util:to_list(A0), SA = split_origin(A), - + if SO == SA -> A; true ->check_origin(R, SO) end. diff --git a/test/etap/252-cors-functionality.t b/test/etap/252-cors-functionality.t index d03486f2d80..c8324177909 100644 --- a/test/etap/252-cors-functionality.t +++ b/test/etap/252-cors-functionality.t @@ -19,13 +19,6 @@ -define(SUPPORTED_METHODS, "GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, COPY, OPTIONS"). -server() -> - lists:concat([ - "http://127.0.0.1:", - mochiweb_socket_server:get(couch_httpd, port), - "/" - ]). - main(_) -> test_util:init_code_path(), @@ -40,33 +33,7 @@ main(_) -> end, ok. -dbname() -> "etap-test-db". -dbname1() -> "etap-test-db1". -dbname2() -> "etap-test-db2". - -admin_user_ctx() -> {user_ctx, #user_ctx{roles=[<<"_admin">>]}}. - -set_admin_password(UserName, Password) -> - Salt = binary_to_list(couch_uuids:random()), - Hashed = couch_util:to_hex(crypto:sha(Password ++ Salt)), - couch_config:set("admins", UserName, - "-hashed-" ++ Hashed ++ "," ++ Salt, false). - - -secobj() -> - {[ - {<<"readers">>, {[{<<"names">>, []}, {<<"roles">>, []}]}}, - {<<"admins">>, {[{<<"names">>, []}, {<<"roles">>, []}]}}, - {<<"origins">>, [<<"http://example.com">>]} - ]}. -secobj2() -> - {[ - {<<"readers">>, {[{<<"names">>, [<<"test">>]}, {<<"roles">>, []}]}}, - {<<"admins">>, {[{<<"names">>, []}, {<<"roles">>, []}]}} - ]}. - - test() -> %% launch couchdb couch_server_sup:start_link(test_util:config_files()), @@ -82,8 +49,8 @@ test() -> {ok, Db1} = couch_db:create(list_to_binary(dbname1()), [admin_user_ctx()]), {ok, Db2} = couch_db:create(list_to_binary(dbname2()), [admin_user_ctx()]), - ok = couch_db:set_security(Db1, secobj()), - ok = couch_db:set_security(Db2, secobj2()), + ok = couch_db:set_security(Db1, secobj()), + ok = couch_db:set_security(Db2, secobj2()), %% do tests test_simple_request(), @@ -96,7 +63,7 @@ test() -> %% do tests with auth ok = set_admin_password("test", "test"), - + test_db_preflight_auth_request(), test_db_origin_auth_request(), @@ -126,10 +93,10 @@ test_simple_request() -> end. test_preflight_request() -> - Headers = [{"Origin", "http://127.0.0.1"}, + Headers = [{"Origin", "http://127.0.0.1"}, {"Access-Control-Request-Method", "GET"}], case ibrowse:send_req(server(), Headers, options, []) of - {ok, _, RespHeaders, _} -> + {ok, _, RespHeaders, _} -> etap:is(proplists:get_value("Access-Control-Allow-Methods", RespHeaders), ?SUPPORTED_METHODS, "Access-Control-Allow-Methods ok"); @@ -151,7 +118,7 @@ test_db_request() -> test_db_preflight_request() -> Url = server() ++ "etap-test-db", - Headers = [{"Origin", "http://127.0.0.1"}, + Headers = [{"Origin", "http://127.0.0.1"}, {"Access-Control-Request-Method", "GET"}], case ibrowse:send_req(Url, Headers, options, []) of {ok, _, RespHeaders, _} -> @@ -203,7 +170,7 @@ test_db1_wrong_origin_request() -> test_db_preflight_auth_request() -> Url = server() ++ "etap-test-db2", - Headers = [{"Origin", "http://127.0.0.1"}, + Headers = [{"Origin", "http://127.0.0.1"}, {"Access-Control-Request-Method", "GET"}], case ibrowse:send_req(Url, Headers, options, []) of {ok, _Status, RespHeaders, _} -> @@ -222,7 +189,7 @@ test_db_origin_auth_request() -> Headers = [{"Origin", "http://127.0.0.1"}], Url = server() ++ "etap-test-db2", - case ibrowse:send_req(Url, Headers, get, [], + case ibrowse:send_req(Url, Headers, get, [], [{basic_auth, {"test", "test"}}]) of {ok, _, RespHeaders, _Body} -> etap:is(proplists:get_value("Access-Control-Allow-Origin", RespHeaders), @@ -232,3 +199,39 @@ test_db_origin_auth_request() -> etap:is(false, true, "ibrowse failed") end. +% +% Utilities +% + +server() -> + lists:concat([ + "http://127.0.0.1:", + mochiweb_socket_server:get(couch_httpd, port), + "/" + ]). + +dbname() -> "cors-test-db". +dbname1() -> "cors-test-db1". +dbname2() -> "cors-test-db2". + +admin_user_ctx() -> {user_ctx, #user_ctx{roles=[<<"_admin">>]}}. + +set_admin_password(UserName, Password) -> + Salt = binary_to_list(couch_uuids:random()), + Hashed = couch_util:to_hex(crypto:sha(Password ++ Salt)), + couch_config:set("admins", UserName, + "-hashed-" ++ Hashed ++ "," ++ Salt, false). + + +secobj() -> + {[ + {<<"readers">>, {[{<<"names">>, []}, {<<"roles">>, []}]}}, + {<<"admins">>, {[{<<"names">>, []}, {<<"roles">>, []}]}}, + {<<"origins">>, [<<"http://example.com">>]} + ]}. + +secobj2() -> + {[ + {<<"readers">>, {[{<<"names">>, [<<"test">>]}, {<<"roles">>, []}]}}, + {<<"admins">>, {[{<<"names">>, []}, {<<"roles">>, []}]}} + ]}. From d732cd2ee206e70f66b86f7f8b4a03d514374a23 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sat, 18 Feb 2012 10:54:37 +0000 Subject: [PATCH 16/29] Refactor the CORS policy check to headers() since it returns headers --- src/couchdb/couch_cors_policy.erl | 8 ++++---- test/etap/250-cors-config.t | 2 +- test/etap/251-cors-policy.t | 24 ++++++++++++------------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index 68990eeedb8..8dc78a29715 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -11,7 +11,7 @@ % the License. -module(couch_cors_policy). --export([global_config/0, check/2, check/3]). +-export([global_config/0, headers/2, headers/3]). % For the test suite. -export([origins_config/3]). @@ -30,10 +30,10 @@ ]). -check(DbConfig, Req) -> - check(global_config(), DbConfig, Req). +headers(DbConfig, Req) -> + headers(global_config(), DbConfig, Req). -check(Global, Local, {Method, Headers}=Req) +headers(Global, Local, {Method, Headers}=Req) when is_list(Global) andalso is_list(Local) andalso is_list(Headers) andalso (is_atom(Method) orelse is_binary(Method)) -> {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), diff --git a/test/etap/250-cors-config.t b/test/etap/250-cors-config.t index ae5cbf345de..fbc9f0669f4 100755 --- a/test/etap/250-cors-config.t +++ b/test/etap/250-cors-config.t @@ -40,7 +40,7 @@ test_api_calls() -> false, "No problem building global CORS policy"), % Test the "shortcut" policy check call, which requires couch_config. - etap:not_ok(threw(fun() -> couch_cors_policy:check([], {'GET', []}) end), + etap:not_ok(threw(fun() -> couch_cors_policy:headers([], {'GET', []}) end), "Policy check with two valid parameters"), ok. diff --git a/test/etap/251-cors-policy.t b/test/etap/251-cors-policy.t index c73be3aaf2b..74000613961 100755 --- a/test/etap/251-cors-policy.t +++ b/test/etap/251-cors-policy.t @@ -11,7 +11,7 @@ % License for the specific language governing permissions and limitations under % the License. -% Test only the policy decisions of couch_cors_policy:check/3--no servers +% Test only the policy decisions of couch_cors_policy:headers/3--no servers % or other couch_* stuff. main(_) -> @@ -37,29 +37,29 @@ test() -> ok. test_bad_api_calls() -> - etap_threw(fun() -> couch_cors_policy:check() end, + etap_threw(fun() -> couch_cors_policy:headers() end, true, "Policy check with zero parameters"), - etap_threw(fun() -> couch_cors_policy:check([]) end, + etap_threw(fun() -> couch_cors_policy:headers([]) end, true, "Policy check with one parameter"), - etap_threw(fun() -> couch_cors_policy:check([], []) end, + etap_threw(fun() -> couch_cors_policy:headers([], []) end, true, "Policy check with two parameters"), - etap_threw(fun() -> couch_cors_policy:check(notList, [], {'GET', []}) end, + etap_threw(fun() -> couch_cors_policy:headers(notList, [], {'GET', []}) end, true, "Policy check with non-list first parameter"), - etap_threw(fun() -> couch_cors_policy:check([], notList, {'GET', []}) end, + etap_threw(fun() -> couch_cors_policy:headers([], notList, {'GET', []}) end, true, "Policy check with non-list second parameter"), - etap_threw(fun() -> couch_cors_policy:check([], [], {{not_method}, []}) end, + etap_threw(fun() -> couch_cors_policy:headers([], [], {{not_method}, []}) end, true, "Policy check with non-method third parameter"), - etap_threw(fun() -> couch_cors_policy:check([], [], {'GET', not_list}) end, + etap_threw(fun() -> couch_cors_policy:headers([], [], {'GET', not_list}) end, true, "Policy check with non-list fourth parameter"), ok. test_good_api_calls() -> - etap_threw(fun() -> couch_cors_policy:check([], [], {'GET', []}) end, + etap_threw(fun() -> couch_cors_policy:headers([], [], {'GET', []}) end, false, "Policy check with three valid parameters"), etap_threw(fun() -> - couch_cors_policy:check(config(), config(), {'GET', []}) + couch_cors_policy:headers(config(), config(), {'GET', []}) end, false, "Policy check with noop configs"), % And the shortcut function. @@ -115,7 +115,7 @@ test_default_policy() -> Config = config([enabled, {"example.com","http://origin.com"}]), HeaderIs = fun(Global, Local, Req, Suffix, Expected, Description) -> - Result = couch_cors_policy:check(Global, Local, Req), + Result = couch_cors_policy:headers(Global, Local, Req), etap:ok(is_list(Result), "List returned: " ++ Description), % Protect against crashing in case the result was not a list. @@ -151,7 +151,7 @@ test_duels() -> % check(A, B, C) -> - couch_cors_policy:check(A, B, C). + couch_cors_policy:headers(A, B, C). etap_threw(Function, Expected, Description) -> Result = try Function() of From b4e8074007349b7bd5a23e554b9347772f608c91 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sat, 18 Feb 2012 11:01:00 +0000 Subject: [PATCH 17/29] Support headers/1 with only the request info --- src/couchdb/couch_cors_policy.erl | 7 +++++-- test/etap/250-cors-config.t | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index 8dc78a29715..c41c57cc47c 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -11,7 +11,7 @@ % the License. -module(couch_cors_policy). --export([global_config/0, headers/2, headers/3]). +-export([global_config/0, headers/1, headers/2, headers/3]). % For the test suite. -export([origins_config/3]). @@ -30,7 +30,10 @@ ]). -headers(DbConfig, Req) -> +headers({_Method, _Headers}=Req) -> + headers([], Req). + +headers(DbConfig, {_Method, _Headers}=Req) -> headers(global_config(), DbConfig, Req). headers(Global, Local, {Method, Headers}=Req) diff --git a/test/etap/250-cors-config.t b/test/etap/250-cors-config.t index fbc9f0669f4..8c2c8bf1e0c 100755 --- a/test/etap/250-cors-config.t +++ b/test/etap/250-cors-config.t @@ -40,8 +40,11 @@ test_api_calls() -> false, "No problem building global CORS policy"), % Test the "shortcut" policy check call, which requires couch_config. - etap:not_ok(threw(fun() -> couch_cors_policy:headers([], {'GET', []}) end), - "Policy check with two valid parameters"), + Req = {'GET', []}, + etap:not_ok(threw(fun() -> couch_cors_policy:headers(Req) end), + "Policy check with one valid parameter"), + etap:not_ok(threw(fun() -> couch_cors_policy:headers([], Req) end), + "Policy check with two valid parameters"), ok. test_policy_structure() -> From 8c85087f18dbf84d1deb72f557395a4d4b1594b4 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sat, 18 Feb 2012 12:20:38 +0000 Subject: [PATCH 18/29] XXX Sat Feb 18 12:20:38 GMT 2012 --- src/couchdb/Makefile.am | 2 ++ src/couchdb/couch_httpd.erl | 38 +++++++++++++++++++++++----- src/couchdb/couch_httpd_db.erl | 19 +++++++++++++- src/couchdb/couch_httpd_external.erl | 3 ++- test/etap/252-cors-functionality.t | 24 +++++++++--------- 5 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am index 68b7dd4e67c..bf20fb3962d 100644 --- a/src/couchdb/Makefile.am +++ b/src/couchdb/Makefile.am @@ -48,6 +48,7 @@ source_files = \ couch_external_server.erl \ couch_file.erl \ couch_httpd.erl \ + couch_httpd_cors.erl \ couch_httpd_db.erl \ couch_httpd_auth.erl \ couch_httpd_oauth.erl \ @@ -105,6 +106,7 @@ compiled_files = \ couch_external_server.beam \ couch_file.beam \ couch_httpd.beam \ + couch_httpd_cors.beam \ couch_httpd_db.beam \ couch_httpd_auth.beam \ couch_httpd_oauth.beam \ diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index 8b05076837d..e34f7de82e5 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -311,9 +311,19 @@ handle_request_int(MochiReq, DefaultFun, HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun), {ok, AuthHandlers} = application:get_env(couch, auth_handlers), + % set default CORS headers + %couch_httpd_cors:set_default_headers(MochiReq), + {ok, Resp} = try case authenticate_request(HttpReq, AuthHandlers) of +% #httpd{method='OPTIONS'} = Req -> +% if HandlerFun =:= DefaultFun -> +% HandlerFun(Req); +% true -> +% couch_httpd_cors:preflight_headers(MochiReq), +% send_json(Req, {[{ok, true}]}) +% end; #httpd{} = Req -> HandlerFun(Req); Response -> @@ -449,8 +459,12 @@ serve_file(Req, RelativePath, DocumentRoot) -> serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot, ExtraHeaders) -> log_request(Req, 200), + ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), + CorsHeaders = couch_cors_policy:headers(Req#httpd.method, ReqHeaders), + io:format("XXX CorsHeaders: ~p\n", [CorsHeaders]), % XXX {ok, MochiReq:serve_file(RelativePath, DocumentRoot, - server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []) ++ ExtraHeaders)}. + server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []) ++ + CorsHeaders ++ ExtraHeaders)}. qs_value(Req, Key) -> qs_value(Req, Key, undefined). @@ -620,7 +634,10 @@ log_request(#httpd{mochi_req=MochiReq,peer=Peer}, Code) -> start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), - Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), Length}), + ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), + Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers) ++ + couch_cors_policy:headers(Req#httpd.method, ReqHeaders), Length}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -630,8 +647,10 @@ start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_cdes, Code}), + ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), + CorsHeaders = couch_cors_policy:headers(Req#httpd.method, ReqHeaders), CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers), - Headers2 = Headers ++ server_header() ++ CookieHeader, + Headers2 = Headers ++ server_header() ++ CookieHeader ++ CorsHeaders, Resp = MochiReq:start_response({Code, Headers2}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); @@ -663,8 +682,11 @@ http_1_0_keep_alive(Req, Headers) -> start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), + ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), + CorsHeaders = couch_cors_policy:headers(Req#httpd.method, ReqHeaders), Headers2 = http_1_0_keep_alive(MochiReq, Headers), - Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers2), chunked}), + Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers2) ++ CorsHeaders, chunked}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -685,12 +707,16 @@ last_chunk(Resp) -> send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), + ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), + CorsHeaders = couch_cors_policy:headers(Req#httpd.method, ReqHeaders), Headers2 = http_1_0_keep_alive(MochiReq, Headers), if Code >= 400 -> ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]); true -> ok end, - {ok, MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers2), Body})}. + {ok, MochiReq:respond({Code, Headers2 ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers2) ++ + CorsHeaders, Body})}. send_method_not_allowed(Req, Methods) -> send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")). @@ -1087,5 +1113,3 @@ partial_find(B, D, N, K) -> _ -> partial_find(B, D, 1 + N, K - 1) end. - - diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index e9e41090544..a7fc653ddc2 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -47,6 +47,22 @@ handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, "You tried to DELETE a database with a ?=rev parameter. " ++ "Did you mean to DELETE a document instead?"}) end; + {'OPTIONS', _} -> + ?LOG_DEBUG("handle cors preflight db request", []), + case couch_db:open_int(DbName, []) of + {ok, Db} -> + try + {SecProps} = couch_db:get_security(Db), + Origins = couch_util:get_value(<<"origins">>, SecProps, + [<<"*">>]), + couch_httpd_cors:preflight_headers(Req, Origins) + after + catch couch_db:close(Db) + end; + _Error -> + couch_httpd_cors:preflight_headers(Req) + end, + couch_httpd:send_json(Req, {[{ok, true}]}); {_, []} -> do_db_req(Req, fun db_req/2); {_, [SecondPart|_]} -> @@ -227,9 +243,10 @@ delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> throw(Error) end. -do_db_req(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Fun) -> +do_db_req(#httpd{user_ctx=UserCtx, path_parts=[DbName|_]}=Req, Fun) -> case couch_db:open(DbName, [{user_ctx, UserCtx}]) of {ok, Db} -> + ok = couch_httpd_cors:db_check_origin(Req, Db), try Fun(Req, Db) after diff --git a/src/couchdb/couch_httpd_external.erl b/src/couchdb/couch_httpd_external.erl index bfe77a329d5..426fcce40cb 100644 --- a/src/couchdb/couch_httpd_external.erl +++ b/src/couchdb/couch_httpd_external.erl @@ -129,7 +129,8 @@ send_external_response(#httpd{mochi_req=MochiReq}=Req, Response) -> } = parse_external_response(Response), couch_httpd:log_request(Req, Code), Resp = MochiReq:respond({Code, - default_or_content_type(CType, Headers ++ couch_httpd:server_header()), Data}), + default_or_content_type(CType, Headers ++ + couch_httpd:server_header() ++ couch_httpd_cors:headers()), Data}), {ok, Resp}. parse_external_response({Response}) -> diff --git a/test/etap/252-cors-functionality.t b/test/etap/252-cors-functionality.t index c8324177909..9be96ac36e1 100644 --- a/test/etap/252-cors-functionality.t +++ b/test/etap/252-cors-functionality.t @@ -54,18 +54,18 @@ test() -> %% do tests test_simple_request(), - test_preflight_request(), - test_db_request(), - test_db_preflight_request(), - test_db_origin_request(), - test_db1_origin_request(), - test_db1_wrong_origin_request(), - - %% do tests with auth - ok = set_admin_password("test", "test"), - - test_db_preflight_auth_request(), - test_db_origin_auth_request(), +% test_preflight_request(), +% test_db_request(), +% test_db_preflight_request(), +% test_db_origin_request(), +% test_db1_origin_request(), +% test_db1_wrong_origin_request(), +% +% %% do tests with auth +% ok = set_admin_password("test", "test"), +% +% test_db_preflight_auth_request(), +% test_db_origin_auth_request(), %% restart boilerplate catch couch_db:close(Db), From 11c35d283ad370bbffd836b2aa043407bd6143aa Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sun, 19 Feb 2012 06:20:40 +0000 Subject: [PATCH 19/29] Display the CORS headers in the debug log --- src/couchdb/couch_cors_policy.erl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index c41c57cc47c..35acbeec490 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -41,7 +41,7 @@ headers(Global, Local, {Method, Headers}=Req) andalso (is_atom(Method) orelse is_binary(Method)) -> {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), Enabled = couch_util:get_value(<<"cors_enabled">>, Httpd, false), - case Enabled of + Result = case Enabled of true -> case lists:keyfind("Origin", 1, Headers) of false -> @@ -55,7 +55,10 @@ headers(Global, Local, {Method, Headers}=Req) end; _ -> [] - end. + end, + + ?LOG_DEBUG("CORS headers: ~p", [Result]), + Result. headers(Global, Local, {ReqMethod, _ReqHeaders}=Req, OriginValue) -> From 504b2e1c00b7144f192687c2425621d74a6bdd99 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sun, 19 Feb 2012 06:21:17 +0000 Subject: [PATCH 20/29] Correctly call the cors policy --- src/couchdb/couch_httpd.erl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index e34f7de82e5..4372a1e0ae2 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -460,8 +460,7 @@ serve_file(Req, RelativePath, DocumentRoot) -> serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot, ExtraHeaders) -> log_request(Req, 200), ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers(Req#httpd.method, ReqHeaders), - io:format("XXX CorsHeaders: ~p\n", [CorsHeaders]), % XXX + CorsHeaders = couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), {ok, MochiReq:serve_file(RelativePath, DocumentRoot, server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []) ++ CorsHeaders ++ ExtraHeaders)}. @@ -637,7 +636,7 @@ start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers) ++ - couch_cors_policy:headers(Req#httpd.method, ReqHeaders), Length}), + couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), Length}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -648,7 +647,7 @@ start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_cdes, Code}), ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers(Req#httpd.method, ReqHeaders), + CorsHeaders = couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers), Headers2 = Headers ++ server_header() ++ CookieHeader ++ CorsHeaders, Resp = MochiReq:start_response({Code, Headers2}), @@ -683,7 +682,7 @@ start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers(Req#httpd.method, ReqHeaders), + CorsHeaders = couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), Headers2 = http_1_0_keep_alive(MochiReq, Headers), Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers2) ++ CorsHeaders, chunked}), @@ -708,7 +707,7 @@ send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers(Req#httpd.method, ReqHeaders), + CorsHeaders = couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), Headers2 = http_1_0_keep_alive(MochiReq, Headers), if Code >= 400 -> ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]); From c89faf9f54917ea1a7698469aee24f244e5ae840 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Sun, 19 Feb 2012 07:03:01 +0000 Subject: [PATCH 21/29] Normalize the list of headers to use string keys --- src/couchdb/couch_cors_policy.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index 35acbeec490..45e597e84db 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -36,13 +36,16 @@ headers({_Method, _Headers}=Req) -> headers(DbConfig, {_Method, _Headers}=Req) -> headers(global_config(), DbConfig, Req). -headers(Global, Local, {Method, Headers}=Req) +headers(Global, Local, {Method, Headers}) when is_list(Global) andalso is_list(Local) andalso is_list(Headers) andalso (is_atom(Method) orelse is_binary(Method)) -> {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), Enabled = couch_util:get_value(<<"cors_enabled">>, Httpd, false), Result = case Enabled of true -> + % Normalize the headers to always use string key names. + StrHeaders = [ {couch_util:to_list(Key), Val} || {Key, Val} <- Headers ], + Req = {Method, StrHeaders}, case lists:keyfind("Origin", 1, Headers) of false -> % s. 5.1(1) and s. 5.2(1) If the Origin header is not From 2dd1fac62f1587ec4c4ee2b5db2eae046a076498 Mon Sep 17 00:00:00 2001 From: "Jason Smith (air)" Date: Mon, 20 Feb 2012 03:55:35 +0000 Subject: [PATCH 22/29] XXX Mon Feb 20 03:55:35 UTC 2012 --- spec.txt | 7 ++ src/couchdb/couch_cors_policy.erl | 187 +++++++++++++++++++++++++----- src/couchdb/couch_httpd.erl | 6 +- src/couchdb/couch_httpd_db.erl | 21 ++-- 4 files changed, 182 insertions(+), 39 deletions(-) diff --git a/spec.txt b/spec.txt index a8a2c31bad4..a530347045a 100644 --- a/spec.txt +++ b/spec.txt @@ -6,6 +6,13 @@ TODO * w3c spec S 5.1.4. Make sure that couch never exposes non-simple headers that are not in AC-Expose-Headers. * Different tests for simple (s5.1) vs. preflight (s5.2) +Tests: + +* Create DB +* Delete DB +* Situations when something is thrown (in do_db_req, etc.) +* IN the config, confirm that headers are always converted to lower case + guidelines : ---------- diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index 45e597e84db..36777e8915a 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -23,7 +23,8 @@ , {<<"max_age">>, 4 * 60 * 60} , {<<"allow_methods">>, <<"GET, HEAD, POST">>} , {<<"allow_headers">>, - <<"Content-Length, If-Match, Destination" + <<"Origin, If-Match, Destination" + , ", Accept, Content-Length, Content-Type" , ", X-HTTP-Method-Override" , ", X-Requested-With" % For jQuery v1.5.1 >>} @@ -65,8 +66,6 @@ headers(Global, Local, {Method, Headers}) headers(Global, Local, {ReqMethod, _ReqHeaders}=Req, OriginValue) -> - Policies = origins_config(Global, Local, Req), - % s. 5 ...each resource is bound to the following: % * A list of origins consisting of zero or more origins that are allowed % to access this resource. @@ -76,10 +75,11 @@ headers(Global, Local, {ReqMethod, _ReqHeaders}=Req, OriginValue) -> % are supported by the resource. % * A supports credentials flag that indicates whether the resource % supports user credentials in the request. [true/false] - Origins = list_of_origins(Policies, Req), - Methods = list_of_methods(Policies, Req), - Headers = list_of_headers(Policies, Req), - Creds = supports_credentials(Policies, Req), + Policy = origins_config(Global, Local, Req), + Origins = list_of_origins(Policy), + Methods = list_of_methods(Policy), + Headers = list_of_headers(Policy), + Creds = supports_credentials(Policy), case ReqMethod of 'OPTIONS' -> @@ -91,13 +91,124 @@ headers(Global, Local, {ReqMethod, _ReqHeaders}=Req, OriginValue) -> end. preflight(OriginVal, OkOrigins, OkMethods, OkHeaders, OkCreds, Req) -> - % Note that s. 5.1(2) (actual requests) requires splitting the Origin + ACRMethod = "Access-Control-Request-Method", + ACRHeaders = "Access-Control-Request-Headers", + + % XXX: s. 5.1(2) (actual requests) requires splitting the Origin % header, and AFAICT all tokens must match the list of origins. But % s. 5.2.2 (preflight requests) implies that the Origin header will contain - % only one token. Assume that the "source origin" (s. 6.1) is the first in - % the list? - SourceOrigin = lists:nth(1, string:tokens(OriginVal, " ")), - []. + % only one token. So is the "source origin" (s. 6.1) the first in + % the list, or the whole header value? + SourceOrigin = OriginVal, + + % s. 5.2(2) If the value of the Origin header is not a case-sensitive match + % for any of the values in [OkOrigins] do not set any additional headers + % and terminate this set of steps. + GoodOrigin = lists:any(fun(Origin) -> + Origin == <<"*">> orelse Origin == SourceOrigin + end, OkOrigins), + + case GoodOrigin of + false -> + ?LOG_DEBUG("Bad preflight origin: ~p vs. (~p)", + [SourceOrigin, OkOrigins]), + []; + true -> + % s. 5.2(3) Let [RequestedMethod] be the value as result of parsing the + % A-C-R-Method header. If there is no A-C-R-Method header or if parsing + % failed, do not set any additional headers and terminate this set of + % steps. + {_Method, ReqHeaders} = Req, + case lists:keyfind(ACRMethod, 1, ReqHeaders) of + false -> + ?LOG_DEBUG("No method for preflight: ~p", SourceOrigin), + []; + {ACRMethod, RequestedMethodStr} -> + RequestedMethod = couch_util:to_binary(RequestedMethodStr), + + % s. 5.2(4) Let [RequestedHeaders] be the values as result of + % parsing the A-C-R-Headers headers. If there are no A-C-R-Headers + % headers let [RequestedHeaders] be the empty list. + RequestedHeaders = case lists:keyfind(ACRHeaders, 1, ReqHeaders) of + false -> []; + {ACRHeaders, HeaderList} -> + comma_split(string:to_lower(HeaderList)) + end, + + ?LOG_DEBUG("Origin ~p requests ~s: ~p", [SourceOrigin, + RequestedMethod, comma_join(RequestedHeaders)]), + + preflight(SourceOrigin, OkCreds, {RequestedMethod, OkMethods}, + {RequestedHeaders, OkHeaders}) + end + end. + +preflight(Origin, OkCreds, {Method, OkMethods}, {Headers, OkHeaders}) -> + % s. 5.2(5) 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. + IsTheMethod = fun(Candidate) -> Candidate =:= Method end, + case lists:any(IsTheMethod, OkMethods) of + false -> + ?LOG_DEBUG("Decline preflight method from ~s: ~s", + [Origin, Method]), + []; + true -> + % s. 5.2(6) If any of the header field-names is not a ASCII + % case-insensitive match for any of the values in list of headers + % do not set any additional headers and terminate this set of + % steps. + BadHeaders = lists:foldl(fun(Header, State) -> + IsThisHeader = fun(Candidate) -> Candidate =:= Header end, + case lists:any(IsThisHeader, OkHeaders) of + true -> State; + false -> [Header | State] + end + end, [], Headers), + + case BadHeaders of + [ _ | _Rest ] -> + ?LOG_DEBUG("Bad preflight headers (~p): ~s", + [Origin, comma_join(BadHeaders)]), + []; + [] -> + % s. 5.2(7) If the resource supports credentials add a + % single A-C-A-Origin header, with the value of the Origin + % header as value, and add a single A-C-A-Credentials + % header with the case-sensitive string "true" as value. + % Otherwise, add a single A-C-A-Origin header, with either + % the value of the Origin header or the string "*" as + % value. + CredsHeaders = case OkCreds of + true -> + [{"Access-Control-Allow-Origin", Origin}, + {"Access-Control-Allow-Credentials", "true"}]; + false -> + [{"Access-Control-Allow-Origin", Origin}] + end, + + % s. 5.2(8) Optionally add a single A-C-Max-Age header with as + % value the amount of seconds the user agent is allowed to + % cache the result of the request. + % + % TODO + MaxAgeHeaders = [], + + % s. 5.2(9) Add one or more Access-Control-Allow-Methods + % headers consisting of (a subset of) the list of methods. + MethodHeaders = [{"Access-Control-Allow-Methods", + comma_join(OkMethods)}], + + % s. 5.2(10) Add one or more A-C-A-Headers headers consisting + % of (a subset of) the list of headers. + HeadersHeaders = [{"Access-Control-Allow-Headers", + comma_join(OkHeaders)}], + + ?LOG_DEBUG("Good preflight ~s: ~p", [Method, Origin]), + CredsHeaders ++ MaxAgeHeaders ++ MethodHeaders + ++ HeadersHeaders + end + end. actual(OriginVal, OkOrigins, _OkMethods, _OkHeaders, OkCreds, _Req) -> % s. 5.1(2) Split the value of the Origin header on the U+0020 SPACE @@ -143,7 +254,7 @@ actual(OriginVal, OkOrigins, _OkMethods, _OkHeaders, OkCreds, _Req) -> % TODO: Merged with the configured expose_headers? Expose = [{"Access-Control-Expose-Headers", - string:join(CouchHeaders, ",")}], + comma_join(CouchHeaders)}], % Interestingly, the spec does not confirm policy for actual % requests. This function ignores methods, headers, and the request @@ -154,17 +265,27 @@ actual(OriginVal, OkOrigins, _OkMethods, _OkHeaders, OkCreds, _Req) -> Allow ++ Expose end. -list_of_origins(Config, _Req) -> - Keys = [ Key || {Key, _Val} <- Config ]. +% Note, the list_of_* functions should receive only one key/val which was built +% by origins_config. +list_of_origins([{Origin, {_Policy}}]) -> + [ Origin ]. -list_of_methods(Config, Req) -> - []. -list_of_headers(Config, Req) -> - []. -supports_credentials(Config, Req) -> - false. +list_of_methods([{_Origin, {Policy}}]) -> + {<<"allow_methods">>, Methods} = lists:keyfind(<<"allow_methods">>, + 1, Policy), + comma_split(Methods). -origins_config(Global, Local, {Method, Headers}) -> +list_of_headers([{_Origin, {Policy}}]) -> + {<<"allow_headers">>, Headers} = lists:keyfind(<<"allow_headers">>, + 1, Policy), + comma_split(Headers). + +supports_credentials([{_Origin, {Policy}}]) -> + {<<"allow_credentials">>, Allowed} = + lists:keyfind(<<"allow_credentials">>, 1, Policy), + Allowed. + +origins_config(Global, Local, {_Method, Headers}) -> % Identify the "origins" configuration object which applies to this % request. The local (i.e. _security object) config takes precidence. {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), @@ -203,9 +324,16 @@ origins_config(BaseOrigins) -> % Normalize the config object for origins, apply defaults, etc. If no % origins are specified, provide a default wildcard entry. Defaulted = fun(Policy) -> - lists:foldl(fun({Key, Val}, State) -> - lists:keyreplace(Key, 1, State, {Key, Val}) - end, ?DEFAULT_CORS_POLICY, Policy) + Policy1 = lists:foldl(fun({Key, Val}, State) -> + lists:keystore(Key, 1, State, {Key, Val}) + end, ?DEFAULT_CORS_POLICY, Policy), + + % Convert the headers to lower case. + {<<"allow_headers">>, Headers} = + lists:keyfind(<<"allow_headers">>, 1, Policy1), + LowerHeaders = string:to_lower(?b2l(Headers)), + lists:keystore(<<"allow_headers">>, 1, Policy1, + {<<"allow_headers">>, ?l2b(LowerHeaders)}) end, Origins = case BaseOrigins of @@ -245,7 +373,7 @@ global_config(origins) -> OriginsSection = couch_config:get("origins"), lists:foldl(fun({Key, Val}, State) -> Domain = ?l2b(Key), - Origins = re:split(Val, ",\\s*"), + Origins = comma_split(Val), DomainObj = lists:foldl(fun(Origin, DomainState) -> Policy = binary_section(Origin), lists:keystore(Origin, 1, DomainState, {Origin, {Policy}}) @@ -263,4 +391,11 @@ binary_section(Section) -> end, [ {?l2b(Key), BoolOrBinary(Val)} || {Key, Val} <- SectionStr ]. +comma_split(Str) -> + re:split(Str, ",\\s*"). + +comma_join(List) -> + StrList = [ couch_util:to_list(E) || E <- List ], + string:join(StrList, ","). + % vim: sts=4 sw=4 et diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index 4372a1e0ae2..627524b7f10 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -706,16 +706,14 @@ last_chunk(Resp) -> send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), - ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), Headers2 = http_1_0_keep_alive(MochiReq, Headers), if Code >= 400 -> ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]); true -> ok end, {ok, MochiReq:respond({Code, Headers2 ++ server_header() ++ - couch_httpd_auth:cookie_auth_header(Req, Headers2) ++ - CorsHeaders, Body})}. + couch_httpd_auth:cookie_auth_header(Req, Headers2), + Body})}. send_method_not_allowed(Req, Methods) -> send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")). diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index a7fc653ddc2..d75dc5bab98 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -48,21 +48,25 @@ handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, ++ "Did you mean to DELETE a document instead?"}) end; {'OPTIONS', _} -> - ?LOG_DEBUG("handle cors preflight db request", []), - case couch_db:open_int(DbName, []) of + SecObj = case couch_db:open_int(DbName, []) of {ok, Db} -> try - {SecProps} = couch_db:get_security(Db), - Origins = couch_util:get_value(<<"origins">>, SecProps, - [<<"*">>]), - couch_httpd_cors:preflight_headers(Req, Origins) + {SecProps} = couch_db:get_security(Db), + SecProps + %Origins = couch_util:get_value(<<"origins">>, SecProps, + % [<<"*">>]), + %couch_httpd_cors:preflight_headers(Req, Origins) after catch couch_db:close(Db) end; _Error -> - couch_httpd_cors:preflight_headers(Req) + [] end, - couch_httpd:send_json(Req, {[{ok, true}]}); + + MochiReq = Req#httpd.mochi_req, + ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), + CorsHeaders = couch_cors_policy:headers(SecObj, {Method, ReqHeaders}), + couch_httpd:send_json(Req, 200, CorsHeaders, {[{ok, true}]}); {_, []} -> do_db_req(Req, fun db_req/2); {_, [SecondPart|_]} -> @@ -246,7 +250,6 @@ delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> do_db_req(#httpd{user_ctx=UserCtx, path_parts=[DbName|_]}=Req, Fun) -> case couch_db:open(DbName, [{user_ctx, UserCtx}]) of {ok, Db} -> - ok = couch_httpd_cors:db_check_origin(Req, Db), try Fun(Req, Db) after From 29f39c7173fd90609e115c52b8731d74f44afd37 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Mon, 20 Feb 2012 09:46:47 +0000 Subject: [PATCH 23/29] Preflight pretty much working (no tests) --- spec.txt | 1 + src/couchdb/couch_cors_policy.erl | 263 +++++++++++++++++------------- 2 files changed, 152 insertions(+), 112 deletions(-) diff --git a/spec.txt b/spec.txt index a530347045a..05dfd45f490 100644 --- a/spec.txt +++ b/spec.txt @@ -12,6 +12,7 @@ Tests: * Delete DB * Situations when something is thrown (in do_db_req, etc.) * IN the config, confirm that headers are always converted to lower case +* Only one config is ever sent back guidelines : ---------- diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index 36777e8915a..1d3c73c5725 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -14,7 +14,7 @@ -export([global_config/0, headers/1, headers/2, headers/3]). % For the test suite. --export([origins_config/3]). +-export([origins_config/3, policy_for_request/2]). -include("couch_db.hrl"). @@ -42,73 +42,57 @@ headers(Global, Local, {Method, Headers}) andalso (is_atom(Method) orelse is_binary(Method)) -> {Httpd} = couch_util:get_value(<<"httpd">>, Global, {[]}), Enabled = couch_util:get_value(<<"cors_enabled">>, Httpd, false), + Result = case Enabled of - true -> - % Normalize the headers to always use string key names. - StrHeaders = [ {couch_util:to_list(Key), Val} || {Key, Val} <- Headers ], - Req = {Method, StrHeaders}, - case lists:keyfind("Origin", 1, Headers) of - false -> - % s. 5.1(1) and s. 5.2(1) If the Origin header is not - % present terminate this set of steps. The request is - % outside the scope of this specification. - ?LOG_DEBUG("Not a CORS request", []), - []; - {"Origin", Origin} -> - headers(Global, Local, Req, Origin) - end; - _ -> - [] + _No when _No =/= true -> + []; + true -> + case request_origin(Headers) of + undefined -> + % s. 5.1(1) and s. 5.2(1) If the Origin header is not present + % terminate this set of steps. The request is outside the scope of + % this specification. + %?LOG_DEBUG("Not a CORS request", []), + []; + Origins -> + headers(Global, Local, {Method, Headers}, Origins) + end end, ?LOG_DEBUG("CORS headers: ~p", [Result]), Result. -headers(Global, Local, {ReqMethod, _ReqHeaders}=Req, OriginValue) -> - % s. 5 ...each resource is bound to the following: - % * A list of origins consisting of zero or more origins that are allowed - % to access this resource. - % * A list of methods consisting of zero or more methods that are - % supported by the resource. - % * A list of headers consisting of zero or more header field names that - % are supported by the resource. - % * A supports credentials flag that indicates whether the resource - % supports user credentials in the request. [true/false] - Policy = origins_config(Global, Local, Req), - Origins = list_of_origins(Policy), - Methods = list_of_methods(Policy), - Headers = list_of_headers(Policy), - Creds = supports_credentials(Policy), +headers(Global, Local, {ReqMethod, _ReqHeaders}=Req, Origins) -> + Policies = origins_config(Global, Local, Req), + Policy = policy_for_request(Policies, Req), case ReqMethod of 'OPTIONS' -> % s. 5.2. Preflight Request; also s. 6.1.5(1) - preflight(OriginValue, Origins, Methods, Headers, Creds, Req); + preflight(Policy, Req); _ -> % s. 5.1. Simple X-O Request, Actual Request, and Redirects. - actual(OriginValue, Origins, Methods, Headers, Creds, Req) + actual(Policy, Req) end. -preflight(OriginVal, OkOrigins, OkMethods, OkHeaders, OkCreds, Req) -> +preflight(Policy, {_Method, ReqHeaders}) -> ACRMethod = "Access-Control-Request-Method", ACRHeaders = "Access-Control-Request-Headers", + {OkOrigins, OkMethods, OkHeaders, OkCreds} = Policy, % XXX: s. 5.1(2) (actual requests) requires splitting the Origin % header, and AFAICT all tokens must match the list of origins. But % s. 5.2.2 (preflight requests) implies that the Origin header will contain % only one token. So is the "source origin" (s. 6.1) the first in % the list, or the whole header value? - SourceOrigin = OriginVal, + SourceOrigin = request_origin(ReqHeaders), % s. 5.2(2) If the value of the Origin header is not a case-sensitive match % for any of the values in [OkOrigins] do not set any additional headers % and terminate this set of steps. - GoodOrigin = lists:any(fun(Origin) -> - Origin == <<"*">> orelse Origin == SourceOrigin - end, OkOrigins), - - case GoodOrigin of + IsThisOrigin = fun(OkOrigin) -> OkOrigin =:= SourceOrigin end, + case lists:any(IsThisOrigin, OkOrigins) of false -> ?LOG_DEBUG("Bad preflight origin: ~p vs. (~p)", [SourceOrigin, OkOrigins]), @@ -118,24 +102,23 @@ preflight(OriginVal, OkOrigins, OkMethods, OkHeaders, OkCreds, Req) -> % A-C-R-Method header. If there is no A-C-R-Method header or if parsing % failed, do not set any additional headers and terminate this set of % steps. - {_Method, ReqHeaders} = Req, - case lists:keyfind(ACRMethod, 1, ReqHeaders) of - false -> + case couch_util:get_value(ACRMethod, ReqHeaders) of + undefined -> ?LOG_DEBUG("No method for preflight: ~p", SourceOrigin), []; - {ACRMethod, RequestedMethodStr} -> + RequestedMethodStr -> RequestedMethod = couch_util:to_binary(RequestedMethodStr), % s. 5.2(4) Let [RequestedHeaders] be the values as result of % parsing the A-C-R-Headers headers. If there are no A-C-R-Headers % headers let [RequestedHeaders] be the empty list. - RequestedHeaders = case lists:keyfind(ACRHeaders, 1, ReqHeaders) of - false -> []; - {ACRHeaders, HeaderList} -> - comma_split(string:to_lower(HeaderList)) - end, + RequestedHeaders = + case couch_util:get_value(ACRHeaders, ReqHeaders) of + undefined -> []; + HeaderList -> comma_split(string:to_lower(HeaderList)) + end, - ?LOG_DEBUG("Origin ~p requests ~s: ~p", [SourceOrigin, + ?LOG_DEBUG("Origin ~s requests ~s with ~s", [SourceOrigin, RequestedMethod, comma_join(RequestedHeaders)]), preflight(SourceOrigin, OkCreds, {RequestedMethod, OkMethods}, @@ -204,18 +187,20 @@ preflight(Origin, OkCreds, {Method, OkMethods}, {Headers, OkHeaders}) -> HeadersHeaders = [{"Access-Control-Allow-Headers", comma_join(OkHeaders)}], - ?LOG_DEBUG("Good preflight ~s: ~p", [Method, Origin]), + ?LOG_DEBUG("Good preflight ~s: ~s", [Method, Origin]), CredsHeaders ++ MaxAgeHeaders ++ MethodHeaders ++ HeadersHeaders end end. -actual(OriginVal, OkOrigins, _OkMethods, _OkHeaders, OkCreds, _Req) -> +actual(Policy, Req) -> + {OkOrigins, OkMethods, OkHeaders, OkCreds} = Policy, + % s. 5.1(2) Split the value of the Origin header on the U+0020 SPACE % character and if any of the resulting tokens is not a case-sensitive % match for any of the values in [OkOrigins] do not set any additional % headers and terminate... - Origins = [ ?l2b(O) || O <- string:tokens(OriginVal, " ") ], + Origins = request_origins(Req), GoodOrigin = fun(Origin) -> lists:any(fun(OkOrigin) -> OkOrigin == <<"*">> orelse OkOrigin == Origin @@ -223,67 +208,103 @@ actual(OriginVal, OkOrigins, _OkMethods, _OkHeaders, OkCreds, _Req) -> end, case lists:all(GoodOrigin, Origins) of - false -> - ?LOG_DEBUG("Origin ~p not allowed: ~p", [Origins, OkOrigins]), - []; - true -> - % s. 5.1(3) If the resource supports credentials add a single - % A-C-A-Origin header, with the value of the origin header as - % value, and add a single A-C-A-Credentials header with the literal - % string "true" as value. - % - % Otherwise, add a single A-C-A-Origin header with either the value - % of the Origin header or the literal string "*" as value. - Allow = case OkCreds of - true -> [ {"Access-Control-Allow-Origin", OriginVal} - , {"Access-Control-Allow-Credentials", "true"} - ]; - false -> [ {"Access-Control-Allow-Origin", OriginVal} ] - end, - - % s. 5.1(4) If the resource wants to expose more than just simple - % response headers to the API of the CORS API specification add one - % or more Access-Control-Expose-Headers headers, with as values the - % filed names of the additional headers to expose. - CouchHeaders = [ "Server", "Date" - , "Content-Length" - , "ETag", "Age" - , "Connection" % ? - % Any others? - ], - - % TODO: Merged with the configured expose_headers? - Expose = [{"Access-Control-Expose-Headers", - comma_join(CouchHeaders)}], - - % Interestingly, the spec does not confirm policy for actual - % requests. This function ignores methods, headers, and the request - % object. Of course, disabling CORS on CouchDB, or removing an - % origin from the config would have immediate effect. Perhaps a - % future feature could detect minor changes to the CORS policy and - % purge the cache as mentioned in the 5.1(4) note. - Allow ++ Expose + false -> + ?LOG_DEBUG("Origin not allowed: ~s", [comma_join(Origins)]), + []; + true -> + % s. 5.1(3) If the resource supports credentials add a single + % A-C-A-Origin header, with the value of the origin header as value, + % and add a single A-C-A-Credentials header with the literal string + % "true" as value. + % + % Otherwise, add a single A-C-A-Origin header with either the value of + % the Origin header or the literal string "*" as value. + OriginVal = request_origin(Req), + Allow = case OkCreds of + true -> [ {"Access-Control-Allow-Origin", OriginVal} + , {"Access-Control-Allow-Credentials", "true"} + ]; + false -> [ {"Access-Control-Allow-Origin", OriginVal} ] + end, + + % s. 5.1(4) If the resource wants to expose more than just simple + % response headers to the API of the CORS API specification add one or + % more Access-Control-Expose-Headers headers, with as values the filed + % names of the additional headers to expose. + CouchHeaders = [ "Server", "Date" + , "Content-Length" + , "ETag", "Age" + , "Connection" % ? + % Any others? + ], + + % TODO: Merged with the configured expose_headers? + Expose = [{"Access-Control-Expose-Headers", comma_join(CouchHeaders)}], + + % Interestingly, the spec does not confirm policy for actual requests. + % This function ignores methods, headers, and the request object. Of + % course, disabling CORS on CouchDB, or removing an origin from the + % config would have immediate effect. Perhaps a future feature could + % detect minor changes to the CORS policy and purge the cache as + % mentioned in the 5.1(4) note. + Allow ++ Expose end. -% Note, the list_of_* functions should receive only one key/val which was built -% by origins_config. -list_of_origins([{Origin, {_Policy}}]) -> - [ Origin ]. - -list_of_methods([{_Origin, {Policy}}]) -> - {<<"allow_methods">>, Methods} = lists:keyfind(<<"allow_methods">>, - 1, Policy), - comma_split(Methods). +policy_for_request(Policies, {_Method, ReqHeaders}) -> + % s. 5 ...each resource is bound to the following: + % * A list of origins consisting of zero or more origins that are allowed + % to access this resource. + % * A list of methods consisting of zero or more methods that are + % supported by the resource. + % * A list of headers consisting of zero or more header field names that + % are supported by the resource. + % * A supports credentials flag that indicates whether the resource + % supports user credentials in the request. [true/false] + % + % The spec indicates that each resources has one static policy. However + % CouchDB allows a per-origin policy. This function merges each origin's + % policy into one single policy appropriate to the request. + ReqOrigins = request_origins(ReqHeaders), + + % Check() (passed to foldl) looks up the policy for a given origin and + % mixes it in to the final policy. + Check = fun(ReqOrigin, {OkOrigins, OkMethods, OkHeaders, OkCreds}=State) -> + {Policy} = case couch_util:get_value(ReqOrigin, Policies) of + undefined -> couch_util:get_value(<<"*">>, Policies); + MatchedPolicy -> MatchedPolicy + end, + + Origins1 = sets:add_element(ReqOrigin, OkOrigins), + + CredsPol = couch_util:get_value(<<"allow_credentials">>, Policy), + Creds1 = OkCreds andalso CredsPol, + + MethodPol = sets:from_list(comma_split( + couch_util:get_value(<<"allow_methods">>, Policy))), + Methods1 = case sets:size(OkMethods) of + 0 -> MethodPol; + _ -> sets:intersection(OkMethods, MethodPol) + end, + + HeaderPol = sets:from_list(comma_split( + couch_util:get_value(<<"allow_headers">>, Policy))), + Headers1 = case sets:size(OkHeaders) of + 0 -> HeaderPol; + _ -> sets:intersection(OkHeaders, HeaderPol) + end, + + {Origins1, Methods1, Headers1, Creds1} + end, -list_of_headers([{_Origin, {Policy}}]) -> - {<<"allow_headers">>, Headers} = lists:keyfind(<<"allow_headers">>, - 1, Policy), - comma_split(Headers). + % origins , methods , headers , allow_credentials + State0 = {sets:new(), sets:new(), sets:new(), false}, + {OkOrigins, OkMethods, OkHeaders, OkCreds} = + lists:foldl(Check, State0, ReqOrigins), -supports_credentials([{_Origin, {Policy}}]) -> - {<<"allow_credentials">>, Allowed} = - lists:keyfind(<<"allow_credentials">>, 1, Policy), - Allowed. + {sets:to_list(OkOrigins), + sets:to_list(OkMethods), + sets:to_list(OkHeaders), + OkCreds}. origins_config(Global, Local, {_Method, Headers}) -> % Identify the "origins" configuration object which applies to this @@ -307,7 +328,6 @@ origins_config(Global, Local, {_Method, Headers}) -> ?LOG_DEBUG("Local origin list: ~s", [VHost]), LocalObj; _ -> - ?LOG_DEBUG("No local origin list: ~s", [VHost]), case couch_util:get_value(VHost, GlobalHosts) of {GlobalObj} -> ?LOG_DEBUG("Global origin list: ~s", [VHost]), @@ -391,6 +411,25 @@ binary_section(Section) -> end, [ {?l2b(Key), BoolOrBinary(Val)} || {Key, Val} <- SectionStr ]. + + +% Return the "Origin" header value. +request_origin({_Method, Headers}) -> + request_origin(Headers); +request_origin([]) -> + undefined; +request_origin([{Key, Val} | Rest]) -> + case couch_util:to_list(Key) of + "Origin" -> couch_util:to_binary(Val); + _ -> request_origin(Rest) + end. + +request_origins(Req) -> + case request_origin(Req) of + undefined -> undefined; + Origins -> re:split(Origins, "\s+") + end. + comma_split(Str) -> re:split(Str, ",\\s*"). From c68d62639bfd9489e95f626625bbcee8127b5907 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Tue, 21 Feb 2012 03:59:05 +0000 Subject: [PATCH 24/29] Whitespace fix --- src/couchdb/couch_cors_policy.erl | 80 +++++++++++++++---------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index 1d3c73c5725..b9f89da3e2d 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -150,46 +150,46 @@ preflight(Origin, OkCreds, {Method, OkMethods}, {Headers, OkHeaders}) -> end, [], Headers), case BadHeaders of - [ _ | _Rest ] -> - ?LOG_DEBUG("Bad preflight headers (~p): ~s", - [Origin, comma_join(BadHeaders)]), - []; - [] -> - % s. 5.2(7) If the resource supports credentials add a - % single A-C-A-Origin header, with the value of the Origin - % header as value, and add a single A-C-A-Credentials - % header with the case-sensitive string "true" as value. - % Otherwise, add a single A-C-A-Origin header, with either - % the value of the Origin header or the string "*" as - % value. - CredsHeaders = case OkCreds of - true -> - [{"Access-Control-Allow-Origin", Origin}, - {"Access-Control-Allow-Credentials", "true"}]; - false -> - [{"Access-Control-Allow-Origin", Origin}] - end, - - % s. 5.2(8) Optionally add a single A-C-Max-Age header with as - % value the amount of seconds the user agent is allowed to - % cache the result of the request. - % - % TODO - MaxAgeHeaders = [], - - % s. 5.2(9) Add one or more Access-Control-Allow-Methods - % headers consisting of (a subset of) the list of methods. - MethodHeaders = [{"Access-Control-Allow-Methods", - comma_join(OkMethods)}], - - % s. 5.2(10) Add one or more A-C-A-Headers headers consisting - % of (a subset of) the list of headers. - HeadersHeaders = [{"Access-Control-Allow-Headers", - comma_join(OkHeaders)}], - - ?LOG_DEBUG("Good preflight ~s: ~s", [Method, Origin]), - CredsHeaders ++ MaxAgeHeaders ++ MethodHeaders - ++ HeadersHeaders + [ _ | _Rest ] -> + ?LOG_DEBUG("Bad preflight headers (~p): ~s", + [Origin, comma_join(BadHeaders)]), + []; + [] -> + % s. 5.2(7) If the resource supports credentials add a + % single A-C-A-Origin header, with the value of the Origin + % header as value, and add a single A-C-A-Credentials + % header with the case-sensitive string "true" as value. + % Otherwise, add a single A-C-A-Origin header, with either + % the value of the Origin header or the string "*" as + % value. + CredsHeaders = case OkCreds of + true -> + [{"Access-Control-Allow-Origin", Origin}, + {"Access-Control-Allow-Credentials", "true"}]; + false -> + [{"Access-Control-Allow-Origin", Origin}] + end, + + % s. 5.2(8) Optionally add a single A-C-Max-Age header with as + % value the amount of seconds the user agent is allowed to + % cache the result of the request. + % + % TODO + MaxAgeHeaders = [], + + % s. 5.2(9) Add one or more Access-Control-Allow-Methods + % headers consisting of (a subset of) the list of methods. + MethodHeaders = [{"Access-Control-Allow-Methods", + comma_join(OkMethods)}], + + % s. 5.2(10) Add one or more A-C-A-Headers headers consisting + % of (a subset of) the list of headers. + HeadersHeaders = [{"Access-Control-Allow-Headers", + comma_join(OkHeaders)}], + + ?LOG_DEBUG("Good preflight ~s: ~s", [Method, Origin]), + CredsHeaders ++ MaxAgeHeaders ++ MethodHeaders + ++ HeadersHeaders end end. From d20f118018a1f530210e01fc1dd076dded25ed1b Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Tue, 21 Feb 2012 04:05:49 +0000 Subject: [PATCH 25/29] Explain sending all the headers --- src/couchdb/couch_cors_policy.erl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/couchdb/couch_cors_policy.erl b/src/couchdb/couch_cors_policy.erl index b9f89da3e2d..14a7b46ae80 100644 --- a/src/couchdb/couch_cors_policy.erl +++ b/src/couchdb/couch_cors_policy.erl @@ -177,6 +177,12 @@ preflight(Origin, OkCreds, {Method, OkMethods}, {Headers, OkHeaders}) -> % TODO MaxAgeHeaders = [], + % Send all approved methods and headers, not just those requested. + % The browser might want to make subsequent (allowed) requests for + % this resource but with different approved headers. To prevent + % another preflight request, just send a list of all approved + % headers. + % s. 5.2(9) Add one or more Access-Control-Allow-Methods % headers consisting of (a subset of) the list of methods. MethodHeaders = [{"Access-Control-Allow-Methods", From 97b56e80ef59f4ca8ba8b0c33b5b76809603d5f2 Mon Sep 17 00:00:00 2001 From: "Jason Smith (work)" Date: Tue, 21 Feb 2012 09:59:59 +0000 Subject: [PATCH 26/29] Confirm that headers are always lower-cased in the config --- spec.txt | 1 - test/etap/250-cors-config.t | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/spec.txt b/spec.txt index 05dfd45f490..c3686109a57 100644 --- a/spec.txt +++ b/spec.txt @@ -11,7 +11,6 @@ Tests: * Create DB * Delete DB * Situations when something is thrown (in do_db_req, etc.) -* IN the config, confirm that headers are always converted to lower case * Only one config is ever sent back guidelines : diff --git a/test/etap/250-cors-config.t b/test/etap/250-cors-config.t index 8c2c8bf1e0c..3a93471339d 100755 --- a/test/etap/250-cors-config.t +++ b/test/etap/250-cors-config.t @@ -17,7 +17,7 @@ default_config() -> main(_) -> test_util:init_code_path(), - etap:plan(18), + etap:plan(20), case (catch test()) of ok -> etap:end_tests(); @@ -32,6 +32,7 @@ test() -> couch_config:start_link([default_config()]), test_api_calls(), test_policy_structure(), + test_lowercase_headers(), test_defaults(), ok. @@ -91,6 +92,21 @@ test_policy_structure() -> ok. +test_lowercase_headers() -> + couch_config:set("origins", "example.com", + "http://origin.com, https://origin.com:6984", false), + couch_config:set("http://origin.com", "allow_headers", + "X-Some-Header", false), + + FauxReq = {'GET', [{"Host", "example.com"}]}, + Config = couch_cors_policy:global_config(), + NormalConfig = couch_cors_policy:origins_config(Config, [], FauxReq), + {Policy} = couch_util:get_value(<<"http://origin.com">>, NormalConfig), + Headers = couch_util:get_value(<<"allow_headers">>, Policy), + etap:is(Headers, <<"x-some-header">>, + "CORS config lower-cases the allowed headers"), + ok. + test_defaults() -> Headers = [], Req = {'GET', Headers}, From 9c3f385cd8173b64dc63edb80766e4e786a1985f Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Sat, 16 Jun 2012 11:47:39 +0100 Subject: [PATCH 27/29] Revert httpd changes --- src/couchdb/couch_httpd.erl | 33 +++++----------------------- src/couchdb/couch_httpd_db.erl | 23 +------------------ src/couchdb/couch_httpd_external.erl | 4 ++-- 3 files changed, 8 insertions(+), 52 deletions(-) diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index 627524b7f10..a42dfa1f03e 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -311,19 +311,9 @@ handle_request_int(MochiReq, DefaultFun, HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun), {ok, AuthHandlers} = application:get_env(couch, auth_handlers), - % set default CORS headers - %couch_httpd_cors:set_default_headers(MochiReq), - {ok, Resp} = try case authenticate_request(HttpReq, AuthHandlers) of -% #httpd{method='OPTIONS'} = Req -> -% if HandlerFun =:= DefaultFun -> -% HandlerFun(Req); -% true -> -% couch_httpd_cors:preflight_headers(MochiReq), -% send_json(Req, {[{ok, true}]}) -% end; #httpd{} = Req -> HandlerFun(Req); Response -> @@ -459,11 +449,8 @@ serve_file(Req, RelativePath, DocumentRoot) -> serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot, ExtraHeaders) -> log_request(Req, 200), - ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), {ok, MochiReq:serve_file(RelativePath, DocumentRoot, - server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []) ++ - CorsHeaders ++ ExtraHeaders)}. + server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []) ++ ExtraHeaders)}. qs_value(Req, Key) -> qs_value(Req, Key, undefined). @@ -633,10 +620,7 @@ log_request(#httpd{mochi_req=MochiReq,peer=Peer}, Code) -> start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), - ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ - couch_httpd_auth:cookie_auth_header(Req, Headers) ++ - couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), Length}), + Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), Length}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -646,10 +630,8 @@ start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_cdes, Code}), - ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers), - Headers2 = Headers ++ server_header() ++ CookieHeader ++ CorsHeaders, + Headers2 = Headers ++ server_header() ++ CookieHeader, Resp = MochiReq:start_response({Code, Headers2}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); @@ -681,11 +663,8 @@ http_1_0_keep_alive(Req, Headers) -> start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> log_request(Req, Code), couch_stats_collector:increment({httpd_status_codes, Code}), - ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers({Req#httpd.method, ReqHeaders}), Headers2 = http_1_0_keep_alive(MochiReq, Headers), - Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++ - couch_httpd_auth:cookie_auth_header(Req, Headers2) ++ CorsHeaders, chunked}), + Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers2), chunked}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -711,9 +690,7 @@ send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]); true -> ok end, - {ok, MochiReq:respond({Code, Headers2 ++ server_header() ++ - couch_httpd_auth:cookie_auth_header(Req, Headers2), - Body})}. + {ok, MochiReq:respond({Code, Headers2 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers2), Body})}. send_method_not_allowed(Req, Methods) -> send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")). diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index d75dc5bab98..47bb53fd645 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -47,26 +47,6 @@ handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, "You tried to DELETE a database with a ?=rev parameter. " ++ "Did you mean to DELETE a document instead?"}) end; - {'OPTIONS', _} -> - SecObj = case couch_db:open_int(DbName, []) of - {ok, Db} -> - try - {SecProps} = couch_db:get_security(Db), - SecProps - %Origins = couch_util:get_value(<<"origins">>, SecProps, - % [<<"*">>]), - %couch_httpd_cors:preflight_headers(Req, Origins) - after - catch couch_db:close(Db) - end; - _Error -> - [] - end, - - MochiReq = Req#httpd.mochi_req, - ReqHeaders = mochiweb_headers:to_list(MochiReq:get(headers)), - CorsHeaders = couch_cors_policy:headers(SecObj, {Method, ReqHeaders}), - couch_httpd:send_json(Req, 200, CorsHeaders, {[{ok, true}]}); {_, []} -> do_db_req(Req, fun db_req/2); {_, [SecondPart|_]} -> @@ -247,7 +227,7 @@ delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> throw(Error) end. -do_db_req(#httpd{user_ctx=UserCtx, path_parts=[DbName|_]}=Req, Fun) -> +do_db_req(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Fun) -> case couch_db:open(DbName, [{user_ctx, UserCtx}]) of {ok, Db} -> try @@ -1218,4 +1198,3 @@ validate_attachment_name(Name) -> true -> Name; false -> throw({bad_request, <<"Attachment name is not UTF-8 encoded">>}) end. - diff --git a/src/couchdb/couch_httpd_external.erl b/src/couchdb/couch_httpd_external.erl index 426fcce40cb..95974054abf 100644 --- a/src/couchdb/couch_httpd_external.erl +++ b/src/couchdb/couch_httpd_external.erl @@ -79,7 +79,7 @@ json_req_obj(#httpd{mochi_req=Req, Headers = Req:get(headers), Hlist = mochiweb_headers:to_list(Headers), {ok, Info} = couch_db:get_db_info(Db), - + % add headers... {[{<<"info">>, {Info}}, {<<"id">>, DocId}, @@ -130,7 +130,7 @@ send_external_response(#httpd{mochi_req=MochiReq}=Req, Response) -> couch_httpd:log_request(Req, Code), Resp = MochiReq:respond({Code, default_or_content_type(CType, Headers ++ - couch_httpd:server_header() ++ couch_httpd_cors:headers()), Data}), + couch_httpd:server_header()), Data}), {ok, Resp}. parse_external_response({Response}) -> From 59e8463460c7ffcf4b35d594dcb414bb157072ce Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Sat, 16 Jun 2012 15:27:58 +0100 Subject: [PATCH 28/29] Fixed expected return values to binary --- test/etap/251-cors-policy.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/etap/251-cors-policy.t b/test/etap/251-cors-policy.t index 74000613961..6994100fcdd 100755 --- a/test/etap/251-cors-policy.t +++ b/test/etap/251-cors-policy.t @@ -133,9 +133,9 @@ test_default_policy() -> end, HeaderIs(Enabled, [], req(), "Allow-Origin", - "http://origin.com", "Default CORS policy echoes the header"), + <<"http://origin.com">>, "Default CORS policy echoes the header"), HeaderIs(Enabled, Config, req(), "Allow-Origin", - "http://origin.com", "Satisfied CORS policy echoes the header"), + <<"http://origin.com">>, "Satisfied CORS policy echoes the header"), HeaderIs(Enabled, [], req(), "Allow-Methods", undefined, "Actual response does not send preflight headers"), From 79e0ec4e459a9f9b8c98d00c4119dda2df43e7ca Mon Sep 17 00:00:00 2001 From: Dale Harvey Date: Sat, 16 Jun 2012 15:54:05 +0100 Subject: [PATCH 29/29] Fix type logging status codes --- src/couchdb/couch_httpd.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index a42dfa1f03e..d45cda2ba78 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -629,7 +629,7 @@ start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> log_request(Req, Code), - couch_stats_collector:increment({httpd_status_cdes, Code}), + couch_stats_collector:increment({httpd_status_co=des, Code}), CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers), Headers2 = Headers ++ server_header() ++ CookieHeader, Resp = MochiReq:start_response({Code, Headers2}),