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. > >>>> > >>> > >>> > > >
