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