Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of CORS #949

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions src/cowboy_req.erl
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
-export([has_resp_header/2]).
-export([has_resp_body/1]).
-export([delete_resp_header/2]).
-export([set_cors_headers/2]).
-export([set_cors_preflight_headers/2]).
-export([reply/2]).
-export([reply/3]).
-export([reply/4]).
Expand Down Expand Up @@ -305,6 +307,8 @@ parse_header_fun(<<"accept">>) -> fun cow_http_hd:parse_accept/1;
parse_header_fun(<<"accept-charset">>) -> fun cow_http_hd:parse_accept_charset/1;
parse_header_fun(<<"accept-encoding">>) -> fun cow_http_hd:parse_accept_encoding/1;
parse_header_fun(<<"accept-language">>) -> fun cow_http_hd:parse_accept_language/1;
parse_header_fun(<<"access-control-request-headers">>) -> fun cow_http_hd:parse_access_control_request_headers/1;
parse_header_fun(<<"access-control-request-method">>) -> fun cow_http_hd:parse_access_control_request_method/1;
parse_header_fun(<<"authorization">>) -> fun cow_http_hd:parse_authorization/1;
parse_header_fun(<<"connection">>) -> fun cow_http_hd:parse_connection/1;
parse_header_fun(<<"content-length">>) -> fun cow_http_hd:parse_content_length/1;
Expand All @@ -315,6 +319,7 @@ parse_header_fun(<<"if-match">>) -> fun cow_http_hd:parse_if_match/1;
parse_header_fun(<<"if-modified-since">>) -> fun cow_http_hd:parse_if_modified_since/1;
parse_header_fun(<<"if-none-match">>) -> fun cow_http_hd:parse_if_none_match/1;
parse_header_fun(<<"if-unmodified-since">>) -> fun cow_http_hd:parse_if_unmodified_since/1;
parse_header_fun(<<"origin">>) -> fun cow_http_hd:parse_origin/1;
parse_header_fun(<<"range">>) -> fun cow_http_hd:parse_range/1;
parse_header_fun(<<"sec-websocket-extensions">>) -> fun cow_http_hd:parse_sec_websocket_extensions/1;
parse_header_fun(<<"sec-websocket-protocol">>) -> fun cow_http_hd:parse_sec_websocket_protocol_req/1;
Expand Down Expand Up @@ -666,6 +671,126 @@ delete_resp_header(Name, Req=#http_req{resp_headers=RespHeaders}) ->
RespHeaders2 = lists:keydelete(Name, 1, RespHeaders),
Req#http_req{resp_headers=RespHeaders2}.

-spec set_cors_headers(map(), Req) -> Req when Req :: req().
set_cors_headers(M, Req) ->
try
AllowedOrigins = maps:get(origins, M, []),
Origin =
match_cors_origin(
%% Validating each origin in the list, picking up the first.
case parse_header(<<"origin">>, Req) of
undefined -> throw({bad_origin, undefined, AllowedOrigins});
[H|T] -> _ = [match_cors_origin(Val, AllowedOrigins) || Val <- T], H;
L -> throw({bad_origin, L, AllowedOrigins})
end,
AllowedOrigins),

Req2 = set_cors_allow_credentials(maps:get(credentials, M, false), Origin, Req),
set_cors_exposed_headers(maps:get(exposed_headers, M, []), Req2)
catch throw:_Reason ->
Req
end.

-spec set_cors_preflight_headers(map(), Req) -> Req when Req :: req().
set_cors_preflight_headers(M, Req) ->
try
AllowedOrigins = maps:get(origins, M, []),
Origin =
match_cors_origin(
%% The Origin header can only contain a single origin as the user agent will not follow redirects.
case parse_header(<<"origin">>, Req) of
undefined -> throw({bad_origin, undefined, AllowedOrigins});
[H] -> H;
L -> throw({bad_origin, L, AllowedOrigins})
end,
AllowedOrigins),
Method =
match_cors_method(
parse_header(<<"access-control-request-method">>, Req),
maps:get(methods, M, [])),
Headers =
match_cors_headers(
parse_header(<<"access-control-request-headers">>, Req, []),
maps:get(headers, M, [])),

Req2 = set_cors_allow_credentials(maps:get(credentials, M, false), Origin, Req),
Req3 = set_cors_max_age(maps:get(max_age, M, undefined), Req2),
Req4 = set_cors_allowed_methods([Method], Req3),
set_cors_allowed_headers(Headers, Req4)
catch throw:_Reason ->
Req
end.

-spec set_cors_allow_credentials(boolean(), {binary(), binary(), 0..65535} | reference(), Req) -> Req when Req :: req().
set_cors_allow_credentials(Credentials, Origin, Req) ->
case match_cors_credentials(Credentials, Origin) of
true ->
Req2 = set_resp_header(<<"access-control-allow-origin">>, cow_http_hd:access_control_allow_origin(Origin), Req),
set_resp_header(<<"access-control-allow-credentials">>, cow_http_hd:access_control_allow_credentials(), Req2);
_ ->
set_resp_header(<<"access-control-allow-origin">>, cow_http_hd:access_control_allow_origin(Origin), Req)
end.

-spec set_cors_max_age(non_neg_integer() | undefined, Req) -> Req when Req :: req().
set_cors_max_age(undefined, Req) ->
Req;
set_cors_max_age(Val, Req) ->
set_resp_header(<<"access-control-max-age">>, cow_http_hd:access_control_max_age(Val), Req).

-spec set_cors_allowed_methods([binary()], Req) -> Req when Req :: req().
set_cors_allowed_methods(L, Req) ->
set_resp_header(<<"access-control-allow-methods">>, cow_http_hd:access_control_allow_methods(L), Req).

-spec set_cors_allowed_headers([binary()], Req) -> Req when Req :: req().
set_cors_allowed_headers([], Req) ->
Req;
set_cors_allowed_headers(L, Req) ->
set_resp_header(<<"access-control-allow-headers">>, cow_http_hd:access_control_allow_headers(L), Req).

-spec set_cors_exposed_headers([binary()], Req) -> Req when Req :: req().
set_cors_exposed_headers([], Req) ->
Req;
set_cors_exposed_headers(L, Req) ->
set_resp_header(<<"access-control-expose-headers">>, cow_http_hd:access_control_expose_headers(L), Req).

-spec match_cors_origin(Origin | reference(), [Origin] | Origin | '*')
-> Origin | '*' when Origin :: {binary(), binary(), 0..65535}.
match_cors_origin(Val, '*') when is_reference(Val) ->
'*';
match_cors_origin(Val, '*') ->
Val;
match_cors_origin(Val, Val) ->
Val;
match_cors_origin(Val, AllowedOrigins) when is_list(AllowedOrigins) ->
case lists:member(Val, AllowedOrigins) of
true -> Val;
_ -> throw({nomatch_origin, Val, AllowedOrigins})
end;
match_cors_origin(Val, AllowedOrigins) ->
throw({nomatch_origin, Val, AllowedOrigins}).

-spec match_cors_method(binary() | undefined, [binary()]) -> binary().
match_cors_method(undefined, Methods) ->
throw({bad_method, undefined, Methods});
match_cors_method(Val, AllowedMethods) ->
case lists:member(Val, AllowedMethods) of
true -> Val;
_ -> throw({nomatch_method, Val, AllowedMethods})
end.

-spec match_cors_headers([binary()], [binary()]) -> [binary()].
match_cors_headers(L, AllowedHeaders) ->
[case lists:member(Header, AllowedHeaders) of
false -> throw({nomatch_header, Header, AllowedHeaders});
_ -> Header
end || Header <- L].

-spec match_cors_credentials(boolean(), {binary(), binary(), 0..65535} | reference() | '*') -> boolean().
match_cors_credentials(true, '*') ->
throw({bad_credentials, true, '*'});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guessing this is for 'The string "" cannot be used for a resource that supports credentials.' but I'm not sure this is very useful, more useful would be sending Origin back instead of "" if credentials are allowed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it's how it works now. User will get an Origin back if credentials are allowed. The only way he can get the "*" in the response the parser didn't fully recognize the Origin value or the value was "null" string, thus the parser returned an reference that can't be returned to the user. So we need this validation for the last case.

match_cors_credentials(Val, _) ->
Val.

-spec reply(cowboy:http_status(), Req) -> Req when Req::req().
reply(Status, Req=#http_req{resp_body=Body}) ->
reply(Status, [], Body, Req).
Expand Down
Loading