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 5E4FED206 for ; Thu, 1 Nov 2012 04:35:58 +0000 (UTC) Received: (qmail 70862 invoked by uid 500); 1 Nov 2012 04:35:57 -0000 Delivered-To: apmail-couchdb-dev-archive@couchdb.apache.org Received: (qmail 70743 invoked by uid 500); 1 Nov 2012 04:35:57 -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 70704 invoked by uid 99); 1 Nov 2012 04:35:56 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 01 Nov 2012 04:35:56 +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 (nike.apache.org: domain of paul.joseph.davis@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; Thu, 01 Nov 2012 04:35:48 +0000 Received: by mail-qc0-f180.google.com with SMTP id v28so1403769qcm.11 for ; Wed, 31 Oct 2012 21:35:27 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20120113; h=mime-version:in-reply-to:references:from:date:message-id:subject:to :content-type:content-transfer-encoding; bh=nCBZoBhkaW/pM4ZUgECPdtHQLuC23QNV5GDxqZPqsy8=; b=sXG0oN8lXHU3zFQqorqSlZT8DwV0uclG1eQAWGF4bKnyh8flCQ2qL0xYwbpd1BiicA FH7EtaXCialmWqDLIL3Q5CAyhTZJ1wYRY32tKvfa19M9Iw0r4vL1C1Azund8r3C3654x q9/fhhpg3xQ0v4srojVg0+2tIRQRY9GwJzMgAONEDdCa6q88aoWdCf38B07ssj+px2zm N5Gc5OoOfqrOOSRVpEa+vRQII6l3tHS0OydqvOh/h5MAw1bW1rO6oaMwtV10m19w3F4c bIGSgALaayJUUP6EJagKQY2us0ngVvRzPFUBMqjjksyYeCEsiHFYslJs5FF2ejd5+c7N Q9hA== Received: by 10.224.191.73 with SMTP id dl9mr25994254qab.18.1351744527714; Wed, 31 Oct 2012 21:35:27 -0700 (PDT) MIME-Version: 1.0 Received: by 10.49.24.79 with HTTP; Wed, 31 Oct 2012 21:34:47 -0700 (PDT) In-Reply-To: <26DCFB09-C3A7-4596-8B5E-4E7E40B61BF4@apache.org> References: <20121031234336.4B0DB5154F@tyr.zones.apache.org> <57B93226-49D4-4F20-9991-AC8A50A7F39D@apache.org> <26DCFB09-C3A7-4596-8B5E-4E7E40B61BF4@apache.org> From: Paul Davis Date: Thu, 1 Nov 2012 00:34:47 -0400 Message-ID: Subject: Re: git commit: handle CORS. fix #COUCHDB-431 To: dev@couchdb.apache.org Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: quoted-printable X-Virus-Checked: Checked by ClamAV on apache.org On Wed, Oct 31, 2012 at 8:14 PM, Adam Kocoloski wrote= : > Right, the wiki page for this stuff is http://wiki.apache.org/couchdb/Mer= ge_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 sch= eme? Cheers, > I think there's a git syntax for renaming on a remote. > Adam > > On Oct 31, 2012, at 7:53 PM, Benoit Chesneau wrote: > >> hrmmmm i thought it was ticketnumber_shortdescr.... I didn't read last >> update of the wiki though .. >> >> - beno=EEt >> >> >> On Thu, Nov 1, 2012 at 12:50 AM, Adam Kocoloski wr= ote: >> >>> 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 >>>> >>>> >>>> handle CORS. fix #COUCHDB-431 >>>> >>>> This patch as support of CORS requests and preflights request as a nod= e >>>> 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 >>>> Authored: Thu Nov 1 00:41:00 2012 +0100 >>>> Committer: benoitc >>>> 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/couchd= b/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 =3D false >>>> ; For more socket options, consult Erlang's module 'inet' man page. >>>> ;socket_options =3D [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, tru= e}] >>>> log_max_chunk_size =3D 1000000 >>>> +cors_enable =3D false >>>> >>>> [ssl] >>>> port =3D 6984 >>>> @@ -67,6 +68,26 @@ auth_cache_size =3D 50 ; size is number of cache en= tries >>>> allow_persistent_cookies =3D false ; set to true to allow persistent >>> cookies >>>> iterations =3D 10000 ; iterations for password hashing >>>> >>>> +[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. >>>> +; 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 >>>> >>>> >>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchd= b/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 =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 >>>> >>>> -EXTRA_DIST =3D $(source_files) couch_db.hrl couch_js_functions.hrl >>>> +EXTRA_DIST =3D $(source_files) couch_db.hrl couch_js_functions.hrl >>>> >>>> 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 \ >>>> >>>> >>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchd= b/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 a= n >>> X-HTTP-METHOD-OVERRIDE header >>>> MethodOverride =3D >>> MochiReq:get_primary_header_value("X-HTTP-Method-Override"), >>>> - Method2 =3D case lists:member(MethodOverride, ["GET", "HEAD", "PO= ST", >>> "PUT", "DELETE", "TRACE", "CONNECT", "COPY"]) of >>>> + Method2 =3D case lists:member(MethodOverride, ["GET", "HEAD", "PO= ST", >>>> + "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), >>>> >>>> + ?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, []). >>>> >>>> -serve_file(#httpd{mochi_req=3DMochiReq}=3DReq, RelativePath, Document= Root, >>> ExtraHeaders) -> >>>> +serve_file(#httpd{mochi_req=3DMochiReq}=3DReq, RelativePath, Document= Root, >>>> + 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=3DMochiReq,peer=3DPe= er}, >>> Code) -> >>>> start_response_length(#httpd{mochi_req=3DMochiReq}=3DReq, Code, Header= s, >>> 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, Heade= rs) -> >>>> 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})}. >>>> >>>> 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/couchd= b/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. Se= e >>> 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(Origi= n), >>>> + 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 <- AllSupportedHead= ers], >>>> + >>>> + % 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") o= f >>>> + 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 AccessHead= ers >>> 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. >>>> >>>> >>> http://git-wip-us.apache.org/repos/asf/couchdb/blob/0777262f/src/couchd= b/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 =3D /" 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 =3D {Module, Fun} >>>> %% >>>> %% The function take 2 args : the mochiweb request object and the targ= et >>>> -%%% path. >>>> +%%% path. >>>> >>>> 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, "/"), >>>> >>>> - 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. >>>> >>>> -% default redirect vhost handler >>>> +% default redirect vhost handler >>>> redirect_to_vhost(MochiReq, VhostTarget) -> >>>> Path =3D MochiReq:get(raw_path), >>>> Target =3D append_path(VhostTarget, Path), >>>> >>>> ?LOG_DEBUG("Vhost Target: '~p'~n", [Target]), >>>> >>>> - Headers =3D mochiweb_headers:enter("x-couchdb-vhost-path", Path, >>>> + Headers =3D 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 =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 -> >>>> + ok -> >>>> case bind_vhost(lists:reverse(VHostParts), HostParts, []) o= f >>>> {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 -> >>>> + 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 =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. >>>> - >>>> + >>>> >>>> split_host_port(HostAsString) -> >>>> case string:rchr(HostAsString, $:) of >>>> 0 -> >>>> {split_host(HostAsString), '*'}; >>>> N -> >>>> - HostPart =3D string:substr(HostAsString, 1, N-1), >>>> - case (catch >>> erlang:list_to_integer(string:substr(HostAsString, >>>> + 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) -> >>>> >>>> >>>> parse_var(P) -> >>>> - case P of >>>> + case P of >>>> ":" ++ Var -> >>>> {bind, Var}; >>>> _ -> P >>>> @@ -323,7 +328,7 @@ make_path(Parts) -> >>>> >>>> init(_) -> >>>> ok =3D couch_config:register(fun ?MODULE:config_change/2), >>>> - >>>> + >>>> %% load configuration >>>> {VHostGlobals, VHosts, Fun} =3D load_conf(), >>>> State =3D #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. Se= e >>> 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, g= et, >>> []), >>>> + 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. >>>> >>> >>> >