Skip to content

Commit

Permalink
Rebased benoitcs cors patch
Browse files Browse the repository at this point in the history
  • Loading branch information
daleharvey committed Jun 17, 2012
1 parent 72ea7e3 commit f41d6ed
Show file tree
Hide file tree
Showing 6 changed files with 456 additions and 10 deletions.
2 changes: 2 additions & 0 deletions src/couchdb/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ source_files = \
couch_external_server.erl \
couch_file.erl \
couch_httpd.erl \
couch_httpd_cors.erl \
couch_httpd_db.erl \
couch_httpd_auth.erl \
couch_httpd_oauth.erl \
Expand Down Expand Up @@ -103,6 +104,7 @@ compiled_files = \
couch_external_server.beam \
couch_file.beam \
couch_httpd.beam \
couch_httpd_cors.beam \
couch_httpd_db.beam \
couch_httpd_auth.beam \
couch_httpd_oauth.beam \
Expand Down
30 changes: 22 additions & 8 deletions src/couchdb/couch_httpd.erl
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ 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", "OPTIONS"]) of
true ->
?LOG_INFO("MethodOverride: ~s (real method was ~s)", [MethodOverride, Method1]),
case Method1 of
Expand Down Expand Up @@ -311,9 +311,19 @@ handle_request_int(MochiReq, DefaultFun,
HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun),
{ok, AuthHandlers} = application:get_env(couch, auth_handlers),

% set default CORS headers
couch_httpd_cors:set_default_headers(MochiReq),

{ok, Resp} =
try
case authenticate_request(HttpReq, AuthHandlers) of
#httpd{method='OPTIONS'} = Req ->
if HandlerFun =:= DefaultFun ->
HandlerFun(Req);
true ->
couch_httpd_cors:preflight_headers(MochiReq),
send_json(Req, {[{ok, true}]})
end;
#httpd{} = Req ->
HandlerFun(Req);
Response ->
Expand Down Expand Up @@ -450,7 +460,8 @@ serve_file(Req, RelativePath, DocumentRoot) ->
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)}.
server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []) ++
couch_httpd_cors:headers() ++ ExtraHeaders)}.

qs_value(Req, Key) ->
qs_value(Req, Key, undefined).
Expand Down Expand Up @@ -620,7 +631,9 @@ 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}),
Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++
couch_httpd_auth:cookie_auth_header(Req, Headers) ++
couch_httpd_cors:headers(), Length}),
case MochiReq:get(method) of
'HEAD' -> throw({http_head_abort, Resp});
_ -> ok
Expand All @@ -631,7 +644,7 @@ start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
log_request(Req, Code),
couch_stats_collector:increment({httpd_status_cdes, Code}),
CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers),
Headers2 = Headers ++ server_header() ++ CookieHeader,
Headers2 = Headers ++ server_header() ++ CookieHeader ++ couch_httpd_cors:headers(),
Resp = MochiReq:start_response({Code, Headers2}),
case MochiReq:get(method) of
'HEAD' -> throw({http_head_abort, Resp});
Expand Down Expand Up @@ -664,7 +677,8 @@ 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}),
Resp = MochiReq:respond({Code, Headers2 ++ server_header() ++
couch_httpd_auth:cookie_auth_header(Req, Headers2) ++ couch_httpd_cors:headers(), chunked}),
case MochiReq:get(method) of
'HEAD' -> throw({http_head_abort, Resp});
_ -> ok
Expand All @@ -690,7 +704,9 @@ send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) ->
?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})}.
{ok, MochiReq:respond({Code, Headers2 ++ server_header() ++
couch_httpd_auth:cookie_auth_header(Req, Headers2)
++ couch_httpd_cors:headers(), Body})}.

send_method_not_allowed(Req, Methods) ->
send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only " ++ Methods ++ " allowed")).
Expand Down Expand Up @@ -1087,5 +1103,3 @@ partial_find(B, D, N, K) ->
_ ->
partial_find(B, D, 1 + N, K - 1)
end.


178 changes: 178 additions & 0 deletions src/couchdb/couch_httpd_cors.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
% 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.

%% the cors utilities
%%
%% CORS processing is done by adding CORS headers to the cors_headers
%% variable in the process registry. Then headers are added to the final
%% response headers while processing the response using the cors_headers
%% function.
%%
%% Process:
%% 1. set default cors headers using set_default_headers when couchdb
%% start to process the request
%% 2. on OPTION method, check asked capabilities and eventually return
%% preflight headers. At this steps CORS headers can be [] (capability not
%% handled) or the list of preflight headers like the spec require.
%% preflight headers are set in preflight_headers method.
%% 3. on database request , we check origin header in a list of origin.
%% Eventually we reset cors headers to [] if the origin isn't
%% supported. Db origins are added to teh "origin" member of the
%% security object. db_check_origin function is used to check them.

-module(couch_httpd_cors).

-include("couch_db.hrl").

-define(SUPPORTED_HEADERS, [
%% simple headers
"Accept",
"Accept-Language",
"Content-Type",
"Expires",
"Last-Modified",
"Pragma",
"Origin",
%% couchdb headers
"Content-Length",
"If-Match",
"Destination",
"X-Requested-With",
"X-Http-Method-Override",
"Content-Range"]).

-export([set_default_headers/1, headers/0,
preflight_headers/1, preflight_headers/2,
db_check_origin/2]).

set_default_headers(MochiReq) ->
case MochiReq:get_header_value("Origin") of
undefined ->
erlang:put(cors_headers, []);
Origin ->
DefaultHeaders = [{"Access-Control-Allow-Origin", Origin},
{"Access-Control-Allow-Credentials", "true"}],
erlang:put(cors_headers, DefaultHeaders)
end.

headers() ->
erlang:get(cors_headers).

split_origin(Origin) ->
{Scheme, Netloc, _, _, _} = mochiweb_util:urlsplit(Origin),
{string:to_lower(Scheme), string:to_lower(Netloc)}.

preflight_headers(MochiReq) ->
preflight_headers(MochiReq, [<<"*">>]).

preflight_headers(#httpd{mochi_req=MochiReq}, AcceptedOrigins) ->
preflight_headers(MochiReq, AcceptedOrigins);
preflight_headers(MochiReq, AcceptedOrigins) ->
SupportedMethods = ["GET", "HEAD", "POST", "PUT",
"DELETE", "TRACE", "CONNECT", "COPY", "OPTIONS"],

%% get custom headers
CustomHeaders = re:split(couch_config:get("cors",
"headers",""), "\\s*,\\s*",[{return, list}]),

%% build list of headers to test
AllSupportedHeaders = ?SUPPORTED_HEADERS ++ CustomHeaders,
SupportedHeaders = [string:to_lower(H) || H <- AllSupportedHeaders],

%% get max age
MaxAge = list_to_integer(
couch_config:get("cors", "max_age", "1000")
),

%% reset cors_headers
erlang:put(cors_headers, []),

case MochiReq:get_header_value("Origin") of
undefined -> ok;
Origin ->
%% if origin validate it against accepted origin
case check_origin(AcceptedOrigins, split_origin(Origin)) of
error -> ok; %% don't set any preflight header
_Origin1 ->
?LOG_DEBUG("check preflight cors request", []),

PreflightHeaders0 = [
{"Access-Control-Allow-Origin", Origin},
{"Access-Control-Allow-Credentials", "true"},
{"Access-Control-Max-Age", MaxAge},
{"Access-Control-Allow-Methods", string:join(SupportedMethods, ", ")}
],

%% now check the reqquested method
case MochiReq:get_header_value("Access-Control-Request-Method") of
undefined ->
erlang:put(cors_headers, PreflightHeaders0);
Method ->
case lists:member(Method, SupportedMethods) of
true ->
%% method ok , check headers
{FinalReqHeaders, ReqHeaders} = case MochiReq:get_header_value(
"Access-Control-Request-Headers") 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}],
erlang:put(cors_headers, PreflightHeaders);
_ -> ok
end;
false -> ok
end
end
end
end.

check_origin([], _SO) ->
error;
check_origin([<<"*">>|_], _SO) ->
"*";
check_origin([A0|R], SO) ->
A = couch_util:to_list(A0),
SA = split_origin(A),

if SO == SA -> A;
true ->check_origin(R, SO)
end.

db_check_origin(#httpd{mochi_req=MochiReq}, Db) ->
{SecProps} = couch_db:get_security(Db),
AcceptedOrigins = couch_util:get_value(<<"origins">>, SecProps, [<<"*">>]),
case MochiReq:get_header_value("Origin") of
undefined -> ok;
Origin ->
case check_origin(AcceptedOrigins, split_origin(Origin)) of
error ->
%% reset cors_headers
erlang:put(cors_headers, []);
_Origin1 ->
CorsHeaders = [{"Access-Control-Allow-Origin", Origin},
{"Access-Control-Allow-Credentials", "true"}],
erlang:put(cors_headers, CorsHeaders)
end
end,
ok.
19 changes: 18 additions & 1 deletion src/couchdb/couch_httpd_db.erl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ handle_request(#httpd{path_parts=[DbName|RestParts],method=Method,
"You tried to DELETE a database with a ?=rev parameter. "
++ "Did you mean to DELETE a document instead?"})
end;
{'OPTIONS', _} ->
?LOG_DEBUG("handle cors preflight db request", []),
case couch_db:open_int(DbName, []) of
{ok, Db} ->
try
{SecProps} = couch_db:get_security(Db),
Origins = couch_util:get_value(<<"origins">>, SecProps,
[<<"*">>]),
couch_httpd_cors:preflight_headers(Req, Origins)
after
catch couch_db:close(Db)
end;
_Error ->
couch_httpd_cors:preflight_headers(Req)
end,
couch_httpd:send_json(Req, {[{ok, true}]});
{_, []} ->
do_db_req(Req, fun db_req/2);
{_, [SecondPart|_]} ->
Expand Down Expand Up @@ -227,9 +243,10 @@ delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
throw(Error)
end.

do_db_req(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Fun) ->
do_db_req(#httpd{user_ctx=UserCtx, path_parts=[DbName|_]}=Req, Fun) ->
case couch_db:open(DbName, [{user_ctx, UserCtx}]) of
{ok, Db} ->
ok = couch_httpd_cors:db_check_origin(Req, Db),
try
Fun(Req, Db)
after
Expand Down
3 changes: 2 additions & 1 deletion src/couchdb/couch_httpd_external.erl
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ send_external_response(#httpd{mochi_req=MochiReq}=Req, Response) ->
} = parse_external_response(Response),
couch_httpd:log_request(Req, Code),
Resp = MochiReq:respond({Code,
default_or_content_type(CType, Headers ++ couch_httpd:server_header()), Data}),
default_or_content_type(CType, Headers ++
couch_httpd:server_header() ++ couch_httpd_cors:headers()), Data}),
{ok, Resp}.

parse_external_response({Response}) ->
Expand Down
Loading

0 comments on commit f41d6ed

Please sign in to comment.