Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 2A800D1A2 for ; Sun, 11 Nov 2012 19:24:37 +0000 (UTC) Received: (qmail 77314 invoked by uid 500); 11 Nov 2012 19:24:34 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 76923 invoked by uid 500); 11 Nov 2012 19:24:33 -0000 Mailing-List: contact commits-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 commits@couchdb.apache.org Received: (qmail 75792 invoked by uid 99); 11 Nov 2012 19:24:32 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Sun, 11 Nov 2012 19:24:32 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 4730D53CBE; Sun, 11 Nov 2012 19:24:32 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: jan@apache.org To: commits@couchdb.apache.org X-Mailer: ASF-Git Admin Mailer Subject: [26/28] git commit: handle CORS. fix #COUCHDB-431 Message-Id: <20121111192432.4730D53CBE@tyr.zones.apache.org> Date: Sun, 11 Nov 2012 19:24:32 +0000 (UTC) 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/d1378411 Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/d1378411 Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/d1378411 Branch: refs/heads/431-feature-cors Commit: d137841198d0f667094e1d0eaa93fe0ae96ee772 Parents: 2057b89 Author: benoitc Authored: Thu Nov 1 00:41:00 2012 +0100 Committer: Jan Lehnardt Committed: Sun Nov 11 16:11:14 2012 +0000 ---------------------------------------------------------------------- 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/d1378411/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/d1378411/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/d1378411/src/couchdb/couch_httpd.erl ---------------------------------------------------------------------- diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index eb35ff9..5ac277a 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/d1378411/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/d1378411/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/d1378411/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.