From dev-return-23693-apmail-couchdb-dev-archive=couchdb.apache.org@couchdb.apache.org Wed Oct 31 23:51:05 2012 Return-Path: X-Original-To: apmail-couchdb-dev-archive@www.apache.org Delivered-To: apmail-couchdb-dev-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 09A59DCB7 for ; Wed, 31 Oct 2012 23:51:05 +0000 (UTC) Received: (qmail 7015 invoked by uid 500); 31 Oct 2012 23:51:04 -0000 Delivered-To: apmail-couchdb-dev-archive@couchdb.apache.org Received: (qmail 6987 invoked by uid 500); 31 Oct 2012 23:51:04 -0000 Mailing-List: contact dev-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list dev@couchdb.apache.org Received: (qmail 6978 invoked by uid 99); 31 Oct 2012 23:51:04 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 31 Oct 2012 23:51:04 +0000 X-ASF-Spam-Status: No, hits=-0.7 required=5.0 tests=NORMAL_HTTP_TO_IP,RCVD_IN_DNSWL_LOW,SPF_PASS X-Spam-Check-By: apache.org Received-SPF: pass (athena.apache.org: domain of adam.kocoloski@gmail.com designates 209.85.216.180 as permitted sender) Received: from [209.85.216.180] (HELO mail-qc0-f180.google.com) (209.85.216.180) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 31 Oct 2012 23:50:58 +0000 Received: by mail-qc0-f180.google.com with SMTP id v28so1300989qcm.11 for ; Wed, 31 Oct 2012 16:50:37 -0700 (PDT) Received: by 10.49.105.229 with SMTP id gp5mr30963906qeb.35.1351727437348; Wed, 31 Oct 2012 16:50:37 -0700 (PDT) Received: from [192.168.1.9] (c-76-119-89-178.hsd1.ma.comcast.net. [76.119.89.178]) by mx.google.com with ESMTPS id f12sm2082363qey.5.2012.10.31.16.50.34 (version=TLSv1/SSLv3 cipher=OTHER); Wed, 31 Oct 2012 16:50:35 -0700 (PDT) Content-Type: text/plain; charset=us-ascii Mime-Version: 1.0 (Mac OS X Mail 6.2 \(1499\)) Subject: Re: git commit: handle CORS. fix #COUCHDB-431 From: Adam Kocoloski In-Reply-To: <20121031234336.4B0DB5154F@tyr.zones.apache.org> Date: Wed, 31 Oct 2012 19:50:34 -0400 Content-Transfer-Encoding: quoted-printable Message-Id: <57B93226-49D4-4F20-9991-AC8A50A7F39D@apache.org> References: <20121031234336.4B0DB5154F@tyr.zones.apache.org> To: dev@couchdb.apache.org X-Mailer: Apple Mail (2.1499) X-Virus-Checked: Checked by ClamAV on apache.org 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, benoitc@apache.org wrote: > Updated Branches: > refs/heads/COUCHDB-431_cors [created] 0777262fa >=20 >=20 > handle CORS. fix #COUCHDB-431 >=20 > This patch as support of CORS requests and preflights request as a = node > level. vhosts are supported >=20 >=20 > 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 >=20 > Branch: refs/heads/COUCHDB-431_cors > Commit: 0777262fa291a79555ea23f2ff203d1ae7654547 > Parents: 88c52b2 > Author: benoitc > Authored: Thu Nov 1 00:41:00 2012 +0100 > Committer: benoitc > Committed: Thu Nov 1 00:41:00 2012 +0100 >=20 > ---------------------------------------------------------------------- > 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(-) > ---------------------------------------------------------------------- >=20 >=20 > = http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/etc/couchdb/d= efault.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 =3D false > ; For more socket options, consult Erlang's module 'inet' man page. > ;socket_options =3D [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, = true}] > log_max_chunk_size =3D 1000000 > +cors_enable =3D false >=20 > [ssl] > port =3D 6984 > @@ -67,6 +68,26 @@ auth_cache_size =3D 50 ; size is number of cache = entries > allow_persistent_cookies =3D false ; set to true to allow persistent = cookies > iterations =3D 10000 ; iterations for password hashing >=20 > +[cors] > +allows_credentials =3D false > +; List of origins separated by a comma > +;origins =3D > +; List of accepted headers separated by a comma > +; headers =3D > +; List of accepted methods > +; methods =3D > + > + > +; Configuration for a vhost > +:[cors:example.com] > +; allows_credentials =3D false > +; List of origins separated by a comma > +;origins =3D > +; List of accepted headers separated by a comma > +; headers =3D > +; List of accepted methods > +; methods =3D > + > [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 =3D [{keepalive, true}, {nodelay, = false}] > ;cert_file =3D /full/path/to/server_cert.pem > ; Path to file containing user's private PEM encoded key. > ;key_file =3D /full/path/to/server_key.pem > -; String containing the user's password. Only used if the private = keyfile is password protected.=20 > +; String containing the user's password. Only used if the private = keyfile is password protected. > ;password =3D somepassword > ; Set to true to validate peer certificates. > verify_ssl_certificates =3D false >=20 > = http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/M= akefile.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 =3D \ > 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 =3D \ > couch_work_queue.erl \ > json_stream_parse.erl >=20 > -EXTRA_DIST =3D $(source_files) couch_db.hrl couch_js_functions.hrl=20 > +EXTRA_DIST =3D $(source_files) couch_db.hrl couch_js_functions.hrl >=20 > compiled_files =3D \ > couch.app \ > @@ -106,6 +107,7 @@ compiled_files =3D \ > 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 \ >=20 > = http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/c= ouch_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, >=20 > % allow broken HTTP clients to fake a full method vocabulary with = an X-HTTP-METHOD-OVERRIDE header > MethodOverride =3D = MochiReq:get_primary_header_value("X-HTTP-Method-Override"), > - Method2 =3D case lists:member(MethodOverride, ["GET", "HEAD", = "POST", "PUT", "DELETE", "TRACE", "CONNECT", "COPY"]) of > + Method2 =3D 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 =3D couch_util:dict_find(HandlerKey, UrlHandlers, = DefaultFun), > {ok, AuthHandlers} =3D application:get_env(couch, auth_handlers), >=20 > + ?LOG_INFO("fuck you ~p~n", [Method]), > {ok, Resp} =3D > try > - case authenticate_request(HttpReq, AuthHandlers) of > - #httpd{} =3D Req -> > - HandlerFun(Req); > - Response -> > - Response > + case couch_httpd_cors:is_preflight_request(HttpReq) of > + #httpd{} -> > + case authenticate_request(HttpReq, AuthHandlers) of > + #httpd{} =3D Req -> > + HandlerFun(Req); > + Response -> > + Response > + end; > + Response -> > + Response > end > catch > throw:{http_head_abort, Resp0} -> > @@ -450,10 +459,13 @@ accepted_encodings(#httpd{mochi_req=3DMochiReq}) = -> > serve_file(Req, RelativePath, DocumentRoot) -> > serve_file(Req, RelativePath, DocumentRoot, []). >=20 > -serve_file(#httpd{mochi_req=3DMochiReq}=3DReq, RelativePath, = DocumentRoot, ExtraHeaders) -> > +serve_file(#httpd{mochi_req=3DMochiReq}=3DReq, 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)}. >=20 > qs_value(Req, Key) -> > qs_value(Req, Key, undefined). > @@ -603,7 +615,10 @@ log_request(#httpd{mochi_req=3DMochiReq,peer=3DPeer= }, Code) -> > start_response_length(#httpd{mochi_req=3DMochiReq}=3DReq, Code, = Headers, Length) -> > log_request(Req, Code), > couch_stats_collector:increment({httpd_status_codes, Code}), > - Resp =3D MochiReq:start_response_length({Code, Headers ++ = server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), = Length}), > + Headers1 =3D Headers ++ server_header() ++ > + couch_httpd_auth:cookie_auth_header(Req, Headers) ++ > + couch_httpd_cors:cors_headers(Req), > + Resp =3D 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=3DMochiReq}=3DReq, = Code, Headers) -> > log_request(Req, Code), > couch_stats_collector:increment({httpd_status_codes, Code}), > CookieHeader =3D couch_httpd_auth:cookie_auth_header(Req, = Headers), > - Headers2 =3D Headers ++ server_header() ++ CookieHeader, > + Headers2 =3D Headers ++ server_header() ++ CookieHeader ++ > + couch_httpd_cors:cors_headers(Req), > Resp =3D 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=3DMochiReq}=3DReq, Code, = Headers) -> > log_request(Req, Code), > couch_stats_collector:increment({httpd_status_codes, Code}), > - Headers2 =3D http_1_0_keep_alive(MochiReq, Headers), > - Resp =3D MochiReq:respond({Code, Headers2 ++ server_header() ++ = couch_httpd_auth:cookie_auth_header(Req, Headers2), chunked}), > + Headers1 =3D http_1_0_keep_alive(MochiReq, Headers), > + Headers2 =3D Headers1 ++ server_header() ++ > + couch_httpd_auth:cookie_auth_header(Req, Headers1) ++ > + couch_httpd_cors:cors_headers(Req), > + Resp =3D 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=3DMochiReq}=3DReq, Code, Headers, Body) = -> > log_request(Req, Code), > couch_stats_collector:increment({httpd_status_codes, Code}), > - Headers2 =3D http_1_0_keep_alive(MochiReq, Headers), > + Headers1 =3D http_1_0_keep_alive(MochiReq, Headers), > if Code >=3D 500 -> > ?LOG_ERROR("httpd ~p error response:~n ~s", [Code, Body]); > Code >=3D 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 =3D Headers1 ++ server_header() ++ > + couch_httpd_cors:cors_headers(Req) ++ > + couch_httpd_auth:cookie_auth_header(Req, Headers1), > + > + {ok, MochiReq:respond({Code, Headers2, Body})}. >=20 > send_method_not_allowed(Req, Methods) -> > send_error(Req, 405, [{"Allow", Methods}], = <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")). >=20 > = http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/c= ouch_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=3DMethod}=3DReq) when Method /=3D = 'OPTIONS' -> > + Req; > +is_preflight_request(#httpd{mochi_req=3DMochiReq}=3DReq) -> > + 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=3DMochiReq}) -> > + Host =3D couch_httpd_vhost:host(MochiReq), > + case get_bool_config("httpd", "enable_cors", false) of > + true -> > + AcceptedOrigins =3D 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 =3D allows_credentials(Origin, Host), > + case lists:member(Origin, AcceptedOrigins) of > + true when AllowsCredentials =3D:=3D true -> > + [{"Access-Control-Allow-Origin", Origin}, > + {"Access-Control-Allow-Credentials", "true"}]; > + true -> > + [{"Access-Control-Allow-Origin", Origin}]; > + _ -> > + [] > + end. > + > + > +preflight_request(MochiReq) -> > + Host =3D 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 =3D 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 =3D split_list(cors_config(Host, "methods", > + ?SUPPORTED_METHODS)), > + > + % get supported headers > + AllSupportedHeaders =3D split_list(cors_config(Host, "headers", > + = ?SUPPORTED_HEADERS)), > + > + SupportedHeaders =3D [string:to_lower(H) || H <- = AllSupportedHeaders], > + > + % get max age > + MaxAge =3D cors_config(Host, "max_age", "12345"), > + > + PreflightHeaders0 =3D 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 =3D MochiReq:get_header_value( > + "Access-Control-Request-Headers"), > + {FinalReqHeaders, ReqHeaders} =3D case = AccessHeaders of > + undefined -> {"", []}; > + Headers -> > + % transform header list in something we > + % could check. make sure everything is a > + % list > + RH =3D [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 =3D PreflightHeaders0 ++ > + = [{"Access-Control-Allow-Headers", > + FinalReqHeaders}], > + {ok, PreflightHeaders}; > + _ -> > + false > + end; > + false -> > + false > + end > + end. > + > + > +allows_credentials("*", _Host) -> > + false; > +allows_credentials(_Origin, Host) -> > + Default =3D 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} =3D 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 =3D 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. >=20 > = http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchdb/c= ouch_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 @@ >=20 > -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]). >=20 > -export([init/1, handle_call/3, handle_cast/2, handle_info/2, = terminate/2, code_change/3]). >=20 > @@ -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.=20 > +%% vhost list. > %% > %% Declaration of vhosts take place in the configuration file : > %% > @@ -51,7 +51,7 @@ > %% "*.db.example.com =3D /" will match all cname on top of db > %% examples to the root of the machine. > %% > -%%=20 > +%% > %% Rewriting Hosts to path > %% ----------------------- > %% > @@ -75,7 +75,7 @@ > %% redirect_vhost_handler =3D {Module, Fun} > %% > %% The function take 2 args : the mochiweb request object and the = target > -%%% path.=20 > +%%% path. >=20 > start_link() -> > gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). > @@ -98,15 +98,7 @@ dispatch_host(MochiReq) -> > {"/" ++ VPath, Query, Fragment} =3D = mochiweb_util:urlsplit_path(MochiReq:get(raw_path)), > VPathParts =3D string:tokens(VPath, "/"), >=20 > - XHost =3D couch_config:get("httpd", "x_forwarded_host", = "X-Forwarded-Host"), > - VHost =3D case MochiReq:get_header_value(XHost) of > - undefined -> > - case MochiReq:get_header_value("Host") of > - undefined -> []; > - Value1 -> Value1 > - end; > - Value -> Value > - end, > + VHost =3D host(MochiReq), > {VHostParts, VhostPort} =3D split_host_port(VHost), > FinalMochiReq =3D case try_bind_vhost(VHosts, = lists:reverse(VHostParts), > VhostPort, VPathParts) of > @@ -133,14 +125,14 @@ append_path("/"=3D_Target, "/"=3D_Path) -> > append_path(Target, Path) -> > Target ++ Path. >=20 > -% default redirect vhost handler=20 > +% default redirect vhost handler > redirect_to_vhost(MochiReq, VhostTarget) -> > Path =3D MochiReq:get(raw_path), > Target =3D append_path(VhostTarget, Path), >=20 > ?LOG_DEBUG("Vhost Target: '~p'~n", [Target]), >=20 > - Headers =3D mochiweb_headers:enter("x-couchdb-vhost-path", Path,=20= > + Headers =3D mochiweb_headers:enter("x-couchdb-vhost-path", Path, > MochiReq:get(headers)), >=20 > % build a new mochiweb request > @@ -154,7 +146,7 @@ redirect_to_vhost(MochiReq, VhostTarget) -> > MochiReq1. >=20 > %% 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=20 > +%* 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 =3D 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} =3D VhostSpec, > case bind_port(VPort, Port) of > - ok ->=20 > + ok -> > case bind_vhost(lists:reverse(VHostParts), HostParts, []) = of > {ok, Bindings, Remainings} -> > case bind_path(VPath, PathParts) of > {ok, PathParts1} -> > Path1 =3D make_target(Path, Bindings, = Remainings, []), > {make_path(Path1), make_path(PathParts1)}; > - fail ->=20 > + fail -> > try_bind_vhost(Rest, HostParts, Port, > PathParts) > end; > @@ -193,7 +185,7 @@ try_bind_vhost([VhostSpec|Rest], HostParts, Port, = PathParts) -> >=20 > %% doc: build new patch from bindings. bindings are query args > %% (+ dynamic query rewritten if needed) and bindings found in > -%% bind_path step.=20 > +%% 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) ->=20 > +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(_, _) -> >=20 >=20 > %% create vhost list from ini > + > +host(MochiReq) -> > + XHost =3D 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 =3D lists:foldl(fun > ({_, ""}, Acc) -> > @@ -267,15 +272,15 @@ parse_vhost(Vhost) -> > H1 =3D make_spec(H, []), > {H1, P, string:tokens(Path, "/")} > end. > - =20 > + >=20 > split_host_port(HostAsString) -> > case string:rchr(HostAsString, $:) of > 0 -> > {split_host(HostAsString), '*'}; > N -> > - HostPart =3D string:substr(HostAsString, 1, N-1),=20 > - case (catch = erlang:list_to_integer(string:substr(HostAsString,=20 > + HostPart =3D 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) -> >=20 >=20 > parse_var(P) -> > - case P of=20 > + case P of > ":" ++ Var -> > {bind, Var}; > _ -> P > @@ -323,7 +328,7 @@ make_path(Parts) -> >=20 > init(_) -> > ok =3D couch_config:register(fun ?MODULE:config_change/2), > - =20 > + > %% load configuration > {VHostGlobals, VHosts, Fun} =3D load_conf(), > State =3D #vhosts_state{ >=20 > = 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 =3D null, > + roles =3D [], > + 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=3D[<<"_admin">>]}}. > + > +set_admin_password(UserName, Password) -> > + Salt =3D binary_to_list(couch_uuids:random()), > + Hashed =3D 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} =3D couch_db:create(list_to_binary(dbname()), = [admin_user_ctx()]), > + {ok, Db1} =3D couch_db:create(list_to_binary(dbname1()), = [admin_user_ctx()]), > + {ok, Db2} =3D 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 =3D couch_config:set("httpd", "enable_cors", "true", false), > + ok =3D 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 =3D 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 =3D [{"Origin", "http://127.0.0.1"}], > + {ok, _, Resp, _} =3D 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 =3D [{"Origin", "http://127.0.0.1"}], > + Url =3D server() ++ "etap-test-db", > + {ok, _, Resp, _} =3D 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 =3D [{"Origin", "http://127.0.0.1"}], > + {ok, _, RespHeaders, _} =3D 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 =3D [{"Origin", "http://127.0.0.1"}, > + {"Access-Control-Request-Method", "GET"}], > + {ok, _, RespHeaders, _} =3D ibrowse:send_req(server(), Headers, = options, []), > + etap:is(proplists:get_value("Access-Control-Allow-Origin", = RespHeaders), > + undefined, > + "invalid origin"). > + > +test_preflight_request() -> > + Headers =3D [{"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 =3D [{"Origin", "http://example.com"}], > + Url =3D 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 =3D server() ++ "etap-test-db", > + Headers =3D [{"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 =3D [{"Origin", "http://example.com"}], > + Url =3D 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 =3D [{"Origin", "http://example.com"}], > + Url =3D 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 =3D server() ++ "etap-test-db2", > + Headers =3D [{"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 =3D [{"Origin", "http://example.com"}], > + Url =3D 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. >=20