Benoit, sorry to keep nagging, but one of the specific conclusions of the thread on branch naming that you started earlier today was that the branch name should indicate whether it's a *feature* or a *bugfix*. In this case the syntax would be 431-feature-CORS.
Adam On Nov 1, 2012, at 1:30 AM, Benoit Chesneau <[email protected]> wrote: > The branch have been renamed to 431_cors. I'm not sure it will be renamed > on github too. > > - benoit > > > On Thu, Nov 1, 2012 at 5:34 AM, Paul Davis <[email protected]>wrote: > >> On Wed, Oct 31, 2012 at 8:14 PM, Adam Kocoloski <[email protected]> >> wrote: >>> Right, the wiki page for this stuff is >> http://wiki.apache.org/couchdb/Merge_Procedure which now reads >>> >>>> Please use the ticket number, the type of the branch, along with a very >> short descriptive phrase, for your branch name. >>>> >>>> If the ticket was COUCHDB-1234, and the ticket title was My Cool >> Feature, your branch should be called 1234-feature-cool. If the issue is a >> bug and the branch includes the bug fix, it should be called 1234-fix-cool. >>> >>> Perhaps we should kill this branch and re-upload to follow the naming >> scheme? Cheers, >>> >> >> I think there's a git syntax for renaming on a remote. >> >>> Adam >>> >>> On Oct 31, 2012, at 7:53 PM, Benoit Chesneau <[email protected]> >> wrote: >>> >>>> hrmmmm i thought it was ticketnumber_shortdescr.... I didn't read last >>>> update of the wiki though .. >>>> >>>> - benoƮt >>>> >>>> >>>> On Thu, Nov 1, 2012 at 12:50 AM, Adam Kocoloski <[email protected]> >> wrote: >>>> >>>>> A minor thing -- didn't we just propose earlier today to use a naming >>>>> convention like 431-feature-CORS for these topic branches? >>>>> >>>>> Adam >>>>> >>>>> On Oct 31, 2012, at 7:43 PM, [email protected] wrote: >>>>> >>>>>> Updated Branches: >>>>>> refs/heads/COUCHDB-431_cors [created] 0777262fa >>>>>> >>>>>> >>>>>> handle CORS. fix #COUCHDB-431 >>>>>> >>>>>> This patch as support of CORS requests and preflights request as a >> node >>>>>> level. vhosts are supported >>>>>> >>>>>> >>>>>> Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo >>>>>> Commit: >> http://git-wip-us.apache.org/repos/asf/couchdb/commit/0777262f >>>>>> Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/0777262f >>>>>> Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/0777262f >>>>>> >>>>>> Branch: refs/heads/COUCHDB-431_cors >>>>>> Commit: 0777262fa291a79555ea23f2ff203d1ae7654547 >>>>>> Parents: 88c52b2 >>>>>> Author: benoitc <[email protected]> >>>>>> Authored: Thu Nov 1 00:41:00 2012 +0100 >>>>>> Committer: benoitc <[email protected]> >>>>>> Committed: Thu Nov 1 00:41:00 2012 +0100 >>>>>> >>>>>> ---------------------------------------------------------------------- >>>>>> etc/couchdb/default.ini.tpl.in | 23 +++- >>>>>> src/couchdb/Makefile.am | 4 +- >>>>>> src/couchdb/couch_httpd.erl | 53 ++++++-- >>>>>> src/couchdb/couch_httpd_cors.erl | 230 >> ++++++++++++++++++++++++++++++++ >>>>>> src/couchdb/couch_httpd_vhost.erl | 55 ++++---- >>>>>> test/etap/231_cors.t | 230 >> ++++++++++++++++++++++++++++++++ >>>>>> 6 files changed, 553 insertions(+), 42 deletions(-) >>>>>> ---------------------------------------------------------------------- >>>>>> >>>>>> >>>>>> >>>>> >> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/etc/couchdb/default.ini.tpl.in >>>>>> ---------------------------------------------------------------------- >>>>>> diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/ >>>>> default.ini.tpl.in >>>>>> index 79ece5c..6a32f65 100644 >>>>>> --- a/etc/couchdb/default.ini.tpl.in >>>>>> +++ b/etc/couchdb/default.ini.tpl.in >>>>>> @@ -49,6 +49,7 @@ allow_jsonp = false >>>>>> ; For more socket options, consult Erlang's module 'inet' man page. >>>>>> ;socket_options = [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, >> true}] >>>>>> log_max_chunk_size = 1000000 >>>>>> +cors_enable = false >>>>>> >>>>>> [ssl] >>>>>> port = 6984 >>>>>> @@ -67,6 +68,26 @@ auth_cache_size = 50 ; size is number of cache >> entries >>>>>> allow_persistent_cookies = false ; set to true to allow persistent >>>>> cookies >>>>>> iterations = 10000 ; iterations for password hashing >>>>>> >>>>>> +[cors] >>>>>> +allows_credentials = false >>>>>> +; List of origins separated by a comma >>>>>> +;origins = >>>>>> +; List of accepted headers separated by a comma >>>>>> +; headers = >>>>>> +; List of accepted methods >>>>>> +; methods = >>>>>> + >>>>>> + >>>>>> +; Configuration for a vhost >>>>>> +:[cors:example.com] >>>>>> +; allows_credentials = false >>>>>> +; List of origins separated by a comma >>>>>> +;origins = >>>>>> +; List of accepted headers separated by a comma >>>>>> +; headers = >>>>>> +; List of accepted methods >>>>>> +; methods = >>>>>> + >>>>>> [couch_httpd_oauth] >>>>>> ; If set to 'true', oauth token and consumer secrets will be looked up >>>>>> ; in the authentication database (_users). These secrets are stored in >>>>>> @@ -224,7 +245,7 @@ socket_options = [{keepalive, true}, {nodelay, >>>>> false}] >>>>>> ;cert_file = /full/path/to/server_cert.pem >>>>>> ; Path to file containing user's private PEM encoded key. >>>>>> ;key_file = /full/path/to/server_key.pem >>>>>> -; String containing the user's password. Only used if the private >>>>> keyfile is password protected. >>>>>> +; String containing the user's password. Only used if the private >>>>> keyfile is password protected. >>>>>> ;password = somepassword >>>>>> ; Set to true to validate peer certificates. >>>>>> verify_ssl_certificates = false >>>>>> >>>>>> >>>>> >> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/Makefile.am >>>>>> ---------------------------------------------------------------------- >>>>>> diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am >>>>>> index 5705976..9fe19bc 100644 >>>>>> --- a/src/couchdb/Makefile.am >>>>>> +++ b/src/couchdb/Makefile.am >>>>>> @@ -49,6 +49,7 @@ source_files = \ >>>>>> couch_httpd.erl \ >>>>>> couch_httpd_db.erl \ >>>>>> couch_httpd_auth.erl \ >>>>>> + couch_httpd_cors.erl \ >>>>>> couch_httpd_oauth.erl \ >>>>>> couch_httpd_external.erl \ >>>>>> couch_httpd_misc_handlers.erl \ >>>>>> @@ -79,7 +80,7 @@ source_files = \ >>>>>> couch_work_queue.erl \ >>>>>> json_stream_parse.erl >>>>>> >>>>>> -EXTRA_DIST = $(source_files) couch_db.hrl couch_js_functions.hrl >>>>>> +EXTRA_DIST = $(source_files) couch_db.hrl couch_js_functions.hrl >>>>>> >>>>>> compiled_files = \ >>>>>> couch.app \ >>>>>> @@ -106,6 +107,7 @@ compiled_files = \ >>>>>> couch_httpd_db.beam \ >>>>>> couch_httpd_auth.beam \ >>>>>> couch_httpd_oauth.beam \ >>>>>> + couch_httpd_cors.beam \ >>>>>> couch_httpd_proxy.beam \ >>>>>> couch_httpd_external.beam \ >>>>>> couch_httpd_misc_handlers.beam \ >>>>>> >>>>>> >>>>> >> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd.erl >>>>>> ---------------------------------------------------------------------- >>>>>> diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl >>>>>> index 45ceebc..6bba871 100644 >>>>>> --- a/src/couchdb/couch_httpd.erl >>>>>> +++ b/src/couchdb/couch_httpd.erl >>>>>> @@ -275,7 +275,10 @@ handle_request_int(MochiReq, DefaultFun, >>>>>> >>>>>> % allow broken HTTP clients to fake a full method vocabulary with >> an >>>>> X-HTTP-METHOD-OVERRIDE header >>>>>> MethodOverride = >>>>> MochiReq:get_primary_header_value("X-HTTP-Method-Override"), >>>>>> - Method2 = case lists:member(MethodOverride, ["GET", "HEAD", >> "POST", >>>>> "PUT", "DELETE", "TRACE", "CONNECT", "COPY"]) of >>>>>> + Method2 = case lists:member(MethodOverride, ["GET", "HEAD", >> "POST", >>>>>> + "PUT", "DELETE", >>>>>> + "TRACE", "CONNECT", >>>>>> + "COPY"]) of >>>>>> true -> >>>>>> ?LOG_INFO("MethodOverride: ~s (real method was ~s)", >>>>> [MethodOverride, Method1]), >>>>>> case Method1 of >>>>>> @@ -312,13 +315,19 @@ handle_request_int(MochiReq, DefaultFun, >>>>>> HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, >>>>> DefaultFun), >>>>>> {ok, AuthHandlers} = application:get_env(couch, auth_handlers), >>>>>> >>>>>> + ?LOG_INFO("fuck you ~p~n", [Method]), >>>>>> {ok, Resp} = >>>>>> try >>>>>> - case authenticate_request(HttpReq, AuthHandlers) of >>>>>> - #httpd{} = Req -> >>>>>> - HandlerFun(Req); >>>>>> - Response -> >>>>>> - Response >>>>>> + case couch_httpd_cors:is_preflight_request(HttpReq) of >>>>>> + #httpd{} -> >>>>>> + case authenticate_request(HttpReq, AuthHandlers) of >>>>>> + #httpd{} = Req -> >>>>>> + HandlerFun(Req); >>>>>> + Response -> >>>>>> + Response >>>>>> + end; >>>>>> + Response -> >>>>>> + Response >>>>>> end >>>>>> catch >>>>>> throw:{http_head_abort, Resp0} -> >>>>>> @@ -450,10 +459,13 @@ accepted_encodings(#httpd{mochi_req=MochiReq}) >> -> >>>>>> serve_file(Req, RelativePath, DocumentRoot) -> >>>>>> serve_file(Req, RelativePath, DocumentRoot, []). >>>>>> >>>>>> -serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, >> DocumentRoot, >>>>> ExtraHeaders) -> >>>>>> +serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, >> DocumentRoot, >>>>>> + ExtraHeaders) -> >>>>>> log_request(Req, 200), >>>>>> - {ok, MochiReq:serve_file(RelativePath, DocumentRoot, >>>>>> - server_header() ++ couch_httpd_auth:cookie_auth_header(Req, >> []) >>>>> ++ ExtraHeaders)}. >>>>>> + {ok, MochiReq:serve_file(RelativePath, DocumentRoot, >>>>> server_header() ++ >>>>>> + couch_httpd_cors:cors_headers(Req) ++ >>>>>> + couch_httpd_auth:cookie_auth_header(Req, >>>>> []) ++ >>>>>> + ExtraHeaders)}. >>>>>> >>>>>> qs_value(Req, Key) -> >>>>>> qs_value(Req, Key, undefined). >>>>>> @@ -603,7 +615,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}), >>>>>> + Headers1 = Headers ++ server_header() ++ >>>>>> + couch_httpd_auth:cookie_auth_header(Req, Headers) ++ >>>>>> + couch_httpd_cors:cors_headers(Req), >>>>>> + Resp = MochiReq:start_response_length({Code, Headers1, Length}), >>>>>> case MochiReq:get(method) of >>>>>> 'HEAD' -> throw({http_head_abort, Resp}); >>>>>> _ -> ok >>>>>> @@ -614,7 +629,8 @@ start_response(#httpd{mochi_req=MochiReq}=Req, >> Code, >>>>> Headers) -> >>>>>> log_request(Req, Code), >>>>>> couch_stats_collector:increment({httpd_status_codes, Code}), >>>>>> CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers), >>>>>> - Headers2 = Headers ++ server_header() ++ CookieHeader, >>>>>> + Headers2 = Headers ++ server_header() ++ CookieHeader ++ >>>>>> + couch_httpd_cors:cors_headers(Req), >>>>>> Resp = MochiReq:start_response({Code, Headers2}), >>>>>> case MochiReq:get(method) of >>>>>> 'HEAD' -> throw({http_head_abort, Resp}); >>>>>> @@ -646,8 +662,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}), >>>>>> - Headers2 = http_1_0_keep_alive(MochiReq, Headers), >>>>>> - Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++ >>>>> couch_httpd_auth:cookie_auth_header(Req, Headers2), chunked}), >>>>>> + Headers1 = http_1_0_keep_alive(MochiReq, Headers), >>>>>> + Headers2 = Headers1 ++ server_header() ++ >>>>>> + couch_httpd_auth:cookie_auth_header(Req, Headers1) ++ >>>>>> + couch_httpd_cors:cors_headers(Req), >>>>>> + Resp = MochiReq:respond({Code, Headers2, chunked}), >>>>>> case MochiReq:get(method) of >>>>>> 'HEAD' -> throw({http_head_abort, Resp}); >>>>>> _ -> ok >>>>>> @@ -668,14 +687,18 @@ 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}), >>>>>> - Headers2 = http_1_0_keep_alive(MochiReq, Headers), >>>>>> + Headers1 = http_1_0_keep_alive(MochiReq, Headers), >>>>>> if Code >= 500 -> >>>>>> ?LOG_ERROR("httpd ~p error response:~n ~s", [Code, Body]); >>>>>> 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})}. >>>>>> + Headers2 = Headers1 ++ server_header() ++ >>>>>> + couch_httpd_cors:cors_headers(Req) ++ >>>>>> + couch_httpd_auth:cookie_auth_header(Req, Headers1), >>>>>> + >>>>>> + {ok, MochiReq:respond({Code, Headers2, Body})}. >>>>>> >>>>>> send_method_not_allowed(Req, Methods) -> >>>>>> send_error(Req, 405, [{"Allow", Methods}], >> <<"method_not_allowed">>, >>>>> ?l2b("Only " ++ Methods ++ " allowed")). >>>>>> >>>>>> >>>>> >> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd_cors.erl >>>>>> ---------------------------------------------------------------------- >>>>>> diff --git a/src/couchdb/couch_httpd_cors.erl >>>>> b/src/couchdb/couch_httpd_cors.erl >>>>>> new file mode 100644 >>>>>> index 0000000..69f57ed >>>>>> --- /dev/null >>>>>> +++ b/src/couchdb/couch_httpd_cors.erl >>>>>> @@ -0,0 +1,230 @@ >>>>>> +% 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. >>>>>> + >>>>>> +%% @doc module to handle Cross-Origin Resource Sharing >>>>>> +%% >>>>>> +%% This module handles CROSS requests and preflight request for a >>>>>> +%% couchdb Node. The config is done in the ini file. >>>>>> + >>>>>> + >>>>>> +-module(couch_httpd_cors). >>>>>> + >>>>>> +-include("couch_db.hrl"). >>>>>> + >>>>>> +-export([is_preflight_request/1, cors_headers/1]). >>>>>> + >>>>>> +-define(SUPPORTED_HEADERS, "Accept, Accept-Language, Content-Type," >> ++ >>>>>> + "Expires, Last-Modified, Pragma, Origin, Content-Length," ++ >>>>>> + "If-Match, Destination, X-Requested-With, " ++ >>>>>> + "X-Http-Method-Override, Content-Range"). >>>>>> + >>>>>> +-define(SUPPORTED_METHODS, "GET, HEAD, POST, PUT, DELETE," ++ >>>>>> + "TRACE, CONNECT, COPY, OPTIONS"). >>>>>> + >>>>>> +is_preflight_request(#httpd{method=Method}=Req) when Method /= >>>>> 'OPTIONS' -> >>>>>> + Req; >>>>>> +is_preflight_request(#httpd{mochi_req=MochiReq}=Req) -> >>>>>> + case get_bool_config("httpd", "enable_cors", false) of >>>>>> + true -> >>>>>> + case preflight_request(MochiReq) of >>>>>> + {ok, PreflightHeaders} -> >>>>>> + couch_httpd:send_response(Req, 204, >>>>> PreflightHeaders, <<>>); >>>>>> + _ -> >>>>>> + Req >>>>>> + end; >>>>>> + false -> >>>>>> + Req >>>>>> + end. >>>>>> + >>>>>> + >>>>>> +cors_headers(#httpd{mochi_req=MochiReq}) -> >>>>>> + Host = couch_httpd_vhost:host(MochiReq), >>>>>> + case get_bool_config("httpd", "enable_cors", false) of >>>>>> + true -> >>>>>> + AcceptedOrigins = re:split(cors_config(Host, "origins", >> []), >>>>>> + "\\s*,\\s*", >>>>>> + [trim, {return, list}]), >>>>>> + case MochiReq:get_header_value("Origin") of >>>>>> + undefined -> >>>>>> + []; >>>>>> + <<"*">> -> >>>>>> + handle_cors_headers("*", Host, AcceptedOrigins); >>>>>> + <<"null">> -> >>>>>> + handle_cors_headers("*", Host, AcceptedOrigins); >>>>>> + Origin -> >>>>>> + handle_cors_headers(couch_util:to_list(Origin), >>>>>> + Host, AcceptedOrigins) >>>>>> + end; >>>>>> + false -> >>>>>> + [] >>>>>> + end. >>>>>> + >>>>>> +handle_cors_headers("*", _Host, _AcceptedOrigins) -> >>>>>> + [{"Access-Control-Allow-Origin", "*"}]; >>>>>> +handle_cors_headers(Origin, Host, []) -> >>>>>> + case allows_credentials(Origin, Host) of >>>>>> + true -> >>>>>> + [{"Access-Control-Allow-Origin", Origin}, >>>>>> + {"Access-Control-Allow-Credentials", "true"}]; >>>>>> + false -> >>>>>> + [{"Access-Control-Allow-Origin", Origin}] >>>>>> + end; >>>>>> +handle_cors_headers(Origin, Host, AcceptedOrigins) -> >>>>>> + AllowsCredentials = allows_credentials(Origin, Host), >>>>>> + case lists:member(Origin, AcceptedOrigins) of >>>>>> + true when AllowsCredentials =:= true -> >>>>>> + [{"Access-Control-Allow-Origin", Origin}, >>>>>> + {"Access-Control-Allow-Credentials", "true"}]; >>>>>> + true -> >>>>>> + [{"Access-Control-Allow-Origin", Origin}]; >>>>>> + _ -> >>>>>> + [] >>>>>> + end. >>>>>> + >>>>>> + >>>>>> +preflight_request(MochiReq) -> >>>>>> + Host = couch_httpd_vhost:host(MochiReq), >>>>>> + case MochiReq:get_header_value("Origin") of >>>>>> + undefined -> >>>>>> + MochiReq; >>>>>> + <<"*">> -> >>>>>> + handle_preflight_request("*", Host, MochiReq); >>>>>> + <<"null">> -> >>>>>> + handle_preflight_request("*", Host, MochiReq); >>>>>> + Origin -> >>>>>> + AcceptedOrigins = re:split(cors_config(Host, "origins", >> []), >>>>>> + "\\s*,\\s*", >>>>>> + [trim, {return, list}]), >>>>>> + case AcceptedOrigins of >>>>>> + [] -> >>>>>> + >> handle_preflight_request(couch_util:to_list(Origin), >>>>>> + Host, MochiReq); >>>>>> + _ -> >>>>>> + case lists:member(Origin, AcceptedOrigins) of >>>>>> + true -> >>>>>> + >>>>> handle_preflight_request(couch_util:to_list(Origin), >>>>>> + Host, MochiReq); >>>>>> + false -> >>>>>> + false >>>>>> + end >>>>>> + end >>>>>> + end. >>>>>> + >>>>>> +handle_preflight_request(Origin, Host, MochiReq) -> >>>>>> + %% get supported methods >>>>>> + SupportedMethods = split_list(cors_config(Host, "methods", >>>>>> + ?SUPPORTED_METHODS)), >>>>>> + >>>>>> + % get supported headers >>>>>> + AllSupportedHeaders = split_list(cors_config(Host, "headers", >>>>>> + >> ?SUPPORTED_HEADERS)), >>>>>> + >>>>>> + SupportedHeaders = [string:to_lower(H) || H <- >> AllSupportedHeaders], >>>>>> + >>>>>> + % get max age >>>>>> + MaxAge = cors_config(Host, "max_age", "12345"), >>>>>> + >>>>>> + PreflightHeaders0 = case allows_credentials(Origin, Host) of >>>>>> + true -> >>>>>> + [{"Access-Control-Allow-Origin", Origin}, >>>>>> + {"Access-Control-Allow-Credentials", "true"}, >>>>>> + {"Access-Control-Max-Age", MaxAge}, >>>>>> + {"Access-Control-Allow-Methods", >>>>> string:join(SupportedMethods, >>>>>> + ", ")}]; >>>>>> + false -> >>>>>> + [{"Access-Control-Allow-Origin", Origin}, >>>>>> + {"Access-Control-Max-Age", MaxAge}, >>>>>> + {"Access-Control-Allow-Methods", >>>>> string:join(SupportedMethods, >>>>>> + ", ")}] >>>>>> + end, >>>>>> + >>>>>> + case MochiReq:get_header_value("Access-Control-Request-Method") >> of >>>>>> + undefined -> >>>>>> + {ok, PreflightHeaders0}; >>>>>> + Method -> >>>>>> + case lists:member(Method, SupportedMethods) of >>>>>> + true -> >>>>>> + % method ok , check headers >>>>>> + AccessHeaders = MochiReq:get_header_value( >>>>>> + "Access-Control-Request-Headers"), >>>>>> + {FinalReqHeaders, ReqHeaders} = case >> AccessHeaders >>>>> of >>>>>> + undefined -> {"", []}; >>>>>> + Headers -> >>>>>> + % transform header list in something we >>>>>> + % could check. make sure everything is a >>>>>> + % list >>>>>> + RH = [string:to_lower(H) >>>>>> + || H <- 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}], >>>>>> + {ok, PreflightHeaders}; >>>>>> + _ -> >>>>>> + false >>>>>> + end; >>>>>> + false -> >>>>>> + false >>>>>> + end >>>>>> + end. >>>>>> + >>>>>> + >>>>>> +allows_credentials("*", _Host) -> >>>>>> + false; >>>>>> +allows_credentials(_Origin, Host) -> >>>>>> + Default = get_bool_config("cors", "allows_credentials", >>>>>> + false), >>>>>> + >>>>>> + get_bool_config(cors_section(Host), "allows_credentials", >>>>>> + Default). >>>>>> + >>>>>> + >>>>>> +cors_config(Host, Key, Default) -> >>>>>> + couch_config:get(cors_section(Host), Key, >>>>>> + couch_config:get("cors", Key, Default)). >>>>>> + >>>>>> +cors_section(Host0) -> >>>>>> + {Host, _Port} = split_host_port(Host0), >>>>>> + "cors:" ++ Host. >>>>>> + >>>>>> +get_bool_config(Section, Key, Default) -> >>>>>> + case couch_config:get(Section, Key) of >>>>>> + undefined -> >>>>>> + Default; >>>>>> + "true" -> >>>>>> + true; >>>>>> + "false" -> >>>>>> + false >>>>>> + end. >>>>>> + >>>>>> +split_list(S) -> >>>>>> + re:split(S, "\\s*,\\s*", [trim, {return, list}]). >>>>>> + >>>>>> +split_host_port(HostAsString) -> >>>>>> + case string:rchr(HostAsString, $:) of >>>>>> + 0 -> >>>>>> + {HostAsString, '*'}; >>>>>> + N -> >>>>>> + HostPart = string:substr(HostAsString, 1, N-1), >>>>>> + case (catch >>>>> erlang:list_to_integer(string:substr(HostAsString, >>>>>> + N+1, length(HostAsString)))) of >>>>>> + {'EXIT', _} -> >>>>>> + {HostAsString, '*'}; >>>>>> + Port -> >>>>>> + {HostPart, Port} >>>>>> + end >>>>>> + end. >>>>>> >>>>>> >>>>> >> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/couch_httpd_vhost.erl >>>>>> ---------------------------------------------------------------------- >>>>>> diff --git a/src/couchdb/couch_httpd_vhost.erl >>>>> b/src/couchdb/couch_httpd_vhost.erl >>>>>> index 59f05ce..4c3ebfe 100644 >>>>>> --- a/src/couchdb/couch_httpd_vhost.erl >>>>>> +++ b/src/couchdb/couch_httpd_vhost.erl >>>>>> @@ -15,7 +15,7 @@ >>>>>> >>>>>> -export([start_link/0, config_change/2, reload/0, get_state/0, >>>>> dispatch_host/1]). >>>>>> -export([urlsplit_netloc/2, redirect_to_vhost/2]). >>>>>> - >>>>>> +-export([host/1, split_host_port/1]). >>>>>> >>>>>> -export([init/1, handle_call/3, handle_cast/2, handle_info/2, >>>>> terminate/2, code_change/3]). >>>>>> >>>>>> @@ -32,7 +32,7 @@ >>>>>> %% doc the vhost manager. >>>>>> %% This gen_server keep state of vhosts added to the ini and try to >>>>>> %% match the Host header (or forwarded) against rules built against >>>>>> -%% vhost list. >>>>>> +%% vhost list. >>>>>> %% >>>>>> %% Declaration of vhosts take place in the configuration file : >>>>>> %% >>>>>> @@ -51,7 +51,7 @@ >>>>>> %% "*.db.example.com = /" will match all cname on top of db >>>>>> %% examples to the root of the machine. >>>>>> %% >>>>>> -%% >>>>>> +%% >>>>>> %% Rewriting Hosts to path >>>>>> %% ----------------------- >>>>>> %% >>>>>> @@ -75,7 +75,7 @@ >>>>>> %% redirect_vhost_handler = {Module, Fun} >>>>>> %% >>>>>> %% The function take 2 args : the mochiweb request object and the >> target >>>>>> -%%% path. >>>>>> +%%% path. >>>>>> >>>>>> start_link() -> >>>>>> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). >>>>>> @@ -98,15 +98,7 @@ dispatch_host(MochiReq) -> >>>>>> {"/" ++ VPath, Query, Fragment} = >>>>> mochiweb_util:urlsplit_path(MochiReq:get(raw_path)), >>>>>> VPathParts = string:tokens(VPath, "/"), >>>>>> >>>>>> - XHost = couch_config:get("httpd", "x_forwarded_host", >>>>> "X-Forwarded-Host"), >>>>>> - VHost = case MochiReq:get_header_value(XHost) of >>>>>> - undefined -> >>>>>> - case MochiReq:get_header_value("Host") of >>>>>> - undefined -> []; >>>>>> - Value1 -> Value1 >>>>>> - end; >>>>>> - Value -> Value >>>>>> - end, >>>>>> + VHost = host(MochiReq), >>>>>> {VHostParts, VhostPort} = split_host_port(VHost), >>>>>> FinalMochiReq = case try_bind_vhost(VHosts, >>>>> lists:reverse(VHostParts), >>>>>> VhostPort, VPathParts) of >>>>>> @@ -133,14 +125,14 @@ append_path("/"=_Target, "/"=_Path) -> >>>>>> append_path(Target, Path) -> >>>>>> Target ++ Path. >>>>>> >>>>>> -% default redirect vhost handler >>>>>> +% default redirect vhost handler >>>>>> redirect_to_vhost(MochiReq, VhostTarget) -> >>>>>> Path = MochiReq:get(raw_path), >>>>>> Target = append_path(VhostTarget, Path), >>>>>> >>>>>> ?LOG_DEBUG("Vhost Target: '~p'~n", [Target]), >>>>>> >>>>>> - Headers = mochiweb_headers:enter("x-couchdb-vhost-path", Path, >>>>>> + Headers = mochiweb_headers:enter("x-couchdb-vhost-path", Path, >>>>>> MochiReq:get(headers)), >>>>>> >>>>>> % build a new mochiweb request >>>>>> @@ -154,7 +146,7 @@ redirect_to_vhost(MochiReq, VhostTarget) -> >>>>>> MochiReq1. >>>>>> >>>>>> %% if so, then it will not be rewritten, but will run as a normal >>>>> couchdb request. >>>>>> -%* normally you'd use this for _uuids _utils and a few of the others >>>>> you want to >>>>>> +%* normally you'd use this for _uuids _utils and a few of the others >>>>> you want to >>>>>> %% keep available on vhosts. You can also use it to make databases >>>>> 'global'. >>>>>> vhost_global( VhostGlobals, MochiReq) -> >>>>>> RawUri = MochiReq:get(raw_path), >>>>>> @@ -175,14 +167,14 @@ try_bind_vhost([], _HostParts, _Port, >> _PathParts) >>>>> -> >>>>>> try_bind_vhost([VhostSpec|Rest], HostParts, Port, PathParts) -> >>>>>> {{VHostParts, VPort, VPath}, Path} = VhostSpec, >>>>>> case bind_port(VPort, Port) of >>>>>> - ok -> >>>>>> + ok -> >>>>>> case bind_vhost(lists:reverse(VHostParts), HostParts, []) >> of >>>>>> {ok, Bindings, Remainings} -> >>>>>> case bind_path(VPath, PathParts) of >>>>>> {ok, PathParts1} -> >>>>>> Path1 = make_target(Path, Bindings, >>>>> Remainings, []), >>>>>> {make_path(Path1), make_path(PathParts1)}; >>>>>> - fail -> >>>>>> + fail -> >>>>>> try_bind_vhost(Rest, HostParts, Port, >>>>>> PathParts) >>>>>> end; >>>>>> @@ -193,7 +185,7 @@ try_bind_vhost([VhostSpec|Rest], HostParts, Port, >>>>> PathParts) -> >>>>>> >>>>>> %% doc: build new patch from bindings. bindings are query args >>>>>> %% (+ dynamic query rewritten if needed) and bindings found in >>>>>> -%% bind_path step. >>>>>> +%% bind_path step. >>>>>> %% TODO: merge code with rewrite. But we need to make sure we are >>>>>> %% in string here. >>>>>> make_target([], _Bindings, _Remaining, Acc) -> >>>>>> @@ -223,7 +215,7 @@ bind_vhost([],[], Bindings) -> {ok, Bindings, []}; >>>>>> bind_vhost([?MATCH_ALL], [], _Bindings) -> fail; >>>>>> bind_vhost([?MATCH_ALL], Rest, Bindings) -> {ok, Bindings, Rest}; >>>>>> bind_vhost([], _HostParts, _Bindings) -> fail; >>>>>> -bind_vhost([{bind, Token}|Rest], [Match|RestHost], Bindings) -> >>>>>> +bind_vhost([{bind, Token}|Rest], [Match|RestHost], Bindings) -> >>>>>> bind_vhost(Rest, RestHost, [{{bind, Token}, Match}|Bindings]); >>>>>> bind_vhost([Cname|Rest], [Cname|RestHost], Bindings) -> >>>>>> bind_vhost(Rest, RestHost, Bindings); >>>>>> @@ -243,6 +235,19 @@ bind_path(_, _) -> >>>>>> >>>>>> >>>>>> %% create vhost list from ini >>>>>> + >>>>>> +host(MochiReq) -> >>>>>> + XHost = couch_config:get("httpd", "x_forwarded_host", >>>>>> + "X-Forwarded-Host"), >>>>>> + case MochiReq:get_header_value(XHost) of >>>>>> + undefined -> >>>>>> + case MochiReq:get_header_value("Host") of >>>>>> + undefined -> []; >>>>>> + Value1 -> Value1 >>>>>> + end; >>>>>> + Value -> Value >>>>>> + end. >>>>>> + >>>>>> make_vhosts() -> >>>>>> Vhosts = lists:foldl(fun >>>>>> ({_, ""}, Acc) -> >>>>>> @@ -267,15 +272,15 @@ parse_vhost(Vhost) -> >>>>>> H1 = make_spec(H, []), >>>>>> {H1, P, string:tokens(Path, "/")} >>>>>> end. >>>>>> - >>>>>> + >>>>>> >>>>>> split_host_port(HostAsString) -> >>>>>> case string:rchr(HostAsString, $:) of >>>>>> 0 -> >>>>>> {split_host(HostAsString), '*'}; >>>>>> N -> >>>>>> - HostPart = string:substr(HostAsString, 1, N-1), >>>>>> - case (catch >>>>> erlang:list_to_integer(string:substr(HostAsString, >>>>>> + HostPart = string:substr(HostAsString, 1, N-1), >>>>>> + case (catch >>>>> erlang:list_to_integer(string:substr(HostAsString, >>>>>> N+1, length(HostAsString)))) of >>>>>> {'EXIT', _} -> >>>>>> {split_host(HostAsString), '*'}; >>>>>> @@ -303,7 +308,7 @@ make_spec([P|R], Acc) -> >>>>>> >>>>>> >>>>>> parse_var(P) -> >>>>>> - case P of >>>>>> + case P of >>>>>> ":" ++ Var -> >>>>>> {bind, Var}; >>>>>> _ -> P >>>>>> @@ -323,7 +328,7 @@ make_path(Parts) -> >>>>>> >>>>>> init(_) -> >>>>>> ok = couch_config:register(fun ?MODULE:config_change/2), >>>>>> - >>>>>> + >>>>>> %% load configuration >>>>>> {VHostGlobals, VHosts, Fun} = load_conf(), >>>>>> State = #vhosts_state{ >>>>>> >>>>>> >>>>> >> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/test/etap/231_cors.t >>>>>> ---------------------------------------------------------------------- >>>>>> diff --git a/test/etap/231_cors.t b/test/etap/231_cors.t >>>>>> new file mode 100644 >>>>>> index 0000000..72fc3df >>>>>> --- /dev/null >>>>>> +++ b/test/etap/231_cors.t >>>>>> @@ -0,0 +1,230 @@ >>>>>> +#!/usr/bin/env escript >>>>>> +%% -*- erlang -*- >>>>>> + >>>>>> +% 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(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. >>>>>> + >>>>>> +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). >>>>>> + >>>>>> +test() -> >>>>>> + >>>>>> + ibrowse:start(), >>>>>> + crypto:start(), >>>>>> + >>>>>> + %% launch couchdb >>>>>> + couch_server_sup:start_link(test_util:config_files()), >>>>>> + >>>>>> + %% 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()]), >>>>>> + >>>>>> + % CORS is disabled by default >>>>>> + test_no_headers_server(), >>>>>> + test_no_headers_db(), >>>>>> + >>>>>> + % Now enable CORS >>>>>> + ok = couch_config:set("httpd", "enable_cors", "true", false), >>>>>> + ok = couch_config:set("cors", "origins", "http://example.com", >>>>> false), >>>>>> + >>>>>> + %% do tests >>>>>> + test_incorrect_origin_simple_request(), >>>>>> + test_incorrect_origin_preflight_request(), >>>>>> + >>>>>> + test_preflight_request(), >>>>>> + test_db_request(), >>>>>> + test_db_preflight_request(), >>>>>> + test_db_origin_request(), >>>>>> + test_db1_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. >>>>>> + >>>>>> +%% Cors is disabled, should not return Access-Control-Allow-Origin >>>>>> +test_no_headers_server() -> >>>>>> + Headers = [{"Origin", "http://127.0.0.1"}], >>>>>> + {ok, _, Resp, _} = ibrowse:send_req(server(), Headers, get, []), >>>>>> + etap:is(proplists:get_value("Access-Control-Allow-Origin", Resp), >>>>>> + undefined, "No CORS Headers when disabled"). >>>>>> + >>>>>> +%% Cors is disabled, should not return Access-Control-Allow-Origin >>>>>> +test_no_headers_db() -> >>>>>> + Headers = [{"Origin", "http://127.0.0.1"}], >>>>>> + Url = server() ++ "etap-test-db", >>>>>> + {ok, _, Resp, _} = ibrowse:send_req(Url, Headers, get, []), >>>>>> + etap:is(proplists:get_value("Access-Control-Allow-Origin", Resp), >>>>>> + undefined, "No CORS Headers when disabled"). >>>>>> + >>>>>> +test_incorrect_origin_simple_request() -> >>>>>> + Headers = [{"Origin", "http://127.0.0.1"}], >>>>>> + {ok, _, RespHeaders, _} = ibrowse:send_req(server(), Headers, >> get, >>>>> []), >>>>>> + etap:is(proplists:get_value("Access-Control-Allow-Origin", >>>>> RespHeaders), >>>>>> + undefined, >>>>>> + "Specified invalid origin, no Access"). >>>>>> + >>>>>> +test_incorrect_origin_preflight_request() -> >>>>>> + Headers = [{"Origin", "http://127.0.0.1"}, >>>>>> + {"Access-Control-Request-Method", "GET"}], >>>>>> + {ok, _, RespHeaders, _} = ibrowse:send_req(server(), Headers, >>>>> options, []), >>>>>> + etap:is(proplists:get_value("Access-Control-Allow-Origin", >>>>> RespHeaders), >>>>>> + undefined, >>>>>> + "invalid origin"). >>>>>> + >>>>>> +test_preflight_request() -> >>>>>> + Headers = [{"Origin", "http://example.com"}, >>>>>> + {"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, >>>>>> + "test_preflight_request Access-Control-Allow-Methods >> ok"); >>>>>> + _ -> >>>>>> + etap:is(false, true, "ibrowse failed") >>>>>> + end. >>>>>> + >>>>>> +test_db_request() -> >>>>>> + Headers = [{"Origin", "http://example.com"}], >>>>>> + 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://example.com", >>>>>> + "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://example.com"}, >>>>>> + {"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://example.com"}], >>>>>> + 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://example.com", >>>>>> + "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_db_preflight_auth_request() -> >>>>>> + Url = server() ++ "etap-test-db2", >>>>>> + Headers = [{"Origin", "http://example.com"}, >>>>>> + {"Access-Control-Request-Method", "GET"}], >>>>>> + case ibrowse:send_req(Url, Headers, options, []) of >>>>>> + {ok, _Status, 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://example.com"}], >>>>>> + 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://example.com", >>>>>> + "db origin ok"); >>>>>> + _ -> >>>>>> + etap:is(false, true, "ibrowse failed") >>>>>> + end. >>>>>> >>>>> >>>>> >>> >>
