Skip to content

Commit

Permalink
Merge pull request #134 from permaweb/chore/cors-headers
Browse files Browse the repository at this point in the history
feat: add support for CORS headers used by browsers
  • Loading branch information
samcamwilliams authored Feb 20, 2025
2 parents cff2e31 + 3bdf64b commit 90bf869
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 11 deletions.
31 changes: 28 additions & 3 deletions src/hb_http.erl
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,10 @@ reply(Req, Message, Opts) ->
reply(Req, message_to_status(Message), Message, Opts).
reply(Req, Status, RawMessage, Opts) ->
Message = hb_converge:normalize_keys(RawMessage),
{ok, EncodedHeaders, EncodedBody} = prepare_reply(Message, Opts),
{ok, HeadersBeforeCors, EncodedBody} = prepare_reply(Message, Opts),
% Get the CORS request headers from the message, if they exist.
ReqHdr = cowboy_req:header(<<"access-control-request-headers">>, Req, <<"">>),
EncodedHeaders = add_cors_headers(HeadersBeforeCors, ReqHdr),
?event(http,
{replying,
{status, Status},
Expand All @@ -414,9 +417,23 @@ reply(Req, Status, RawMessage, Opts) ->
}
end,
Req2 = cowboy_req:stream_reply(Status, #{}, SetCookiesReq),
Req3 = cowboy_req:stream_body(EncodedBody, fin, Req2),
Req3 = cowboy_req:stream_body(EncodedBody, nofin, Req2),
{ok, Req3, no_state}.

%% @doc Add permissive CORS headers to a message, if the message has not already
%% specified CORS headers.
add_cors_headers(Msg, ReqHdr) ->
% Keys in the given message will overwrite the defaults listed below if
% included, due to `maps:merge`'s precidence order.
maps:merge(
#{
<<"access-control-allow-origin">> => <<"*">>,
<<"access-control-allow-methods">> => <<"GET, POST, PUT, DELETE, OPTIONS">>,
<<"access-control-allow-headers">> => ReqHdr
},
Msg
).

%% @doc Generate the headers and body for a HTTP response message.
prepare_reply(Message, Opts) ->
case hb_opts:get(format, http, Opts) of
Expand Down Expand Up @@ -622,4 +639,12 @@ get_deep_signed_wasm_state_test() ->
Msg = wasm_compute_request(
<<"test/test-64.wasm">>, <<"fac">>, [3.0], <<"/output">>),
{ok, Res} = post(URL, Msg, #{}),
?assertEqual(6.0, hb_converge:get(<<"1">>, Res, #{})).
?assertEqual(6.0, hb_converge:get(<<"1">>, Res, #{})).

cors_get_test() ->
URL = hb_http_server:start_node(),
{ok, Res} = get(URL, <<"/~meta@1.0/info/address">>, #{}),
?assertEqual(
<<"*">>,
hb_converge:get(<<"access-control-allow-origin">>, Res, #{})
).
32 changes: 24 additions & 8 deletions src/hb_http_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,29 @@ start_http2(ServerID, ProtoOpts, NodeMsg) ->
),
{ok, Port, Listener}.

%% @doc Entrypoint for all HTTP requests. Receives the Cowboy request option and
%% the server ID, which can be used to lookup the node message.
init(Req, ServerID) ->
case cowboy_req:method(Req) of
<<"OPTIONS">> ->
cors_reply(Req, ServerID);
_ ->
handle_request(Req, ServerID)
end.

%% @doc Reply to CORS preflight requests.
cors_reply(Req, _ServerID) ->
cowboy_req:reply(204, #{
<<"access-control-allow-origin">> => <<"*">>,
<<"access-control-allow-methods">> => <<"GET, POST, PUT, DELETE, OPTIONS">>,
<<"access-control-allow-headers">> => <<"*">>
}, Req).

%% @doc Handle all non-CORS preflight requests as Converge requests. Execution
%% starts by parsing the HTTP request into HyerBEAM's message format, then
%% passing the message directly to `meta@1.0` which handles calling Converge in
%% the appropriate way.
handle_request(Req, ServerID) ->
NodeMsg = get_opts(#{ http_server => ServerID }),
?event(http, {http_inbound, Req}),
% Parse the HTTP request into HyerBEAM's message format.
Expand All @@ -164,15 +186,9 @@ init(Req, ServerID) ->
{ok, Res} = dev_meta:handle(NodeMsg, ReqSingleton),
hb_http:reply(Req, Res, NodeMsg).

%% @doc Return the complete Ranch ETS table for the node for debugging.
ranch_ets() ->
case ets:info(ranch_server) of
undefined -> [];
_ -> ets:tab2list(ranch_server)
end.

%% @doc Return the list of allowed methods for the HTTP server.
allowed_methods(Req, State) ->
{[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>], Req, State}.
{[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}.

%% @doc Update the `Opts' map that the HTTP server uses for all future
%% requests.
Expand Down

0 comments on commit 90bf869

Please sign in to comment.