-
Notifications
You must be signed in to change notification settings - Fork 428
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
Allow custom HTTP headers in BOSH and WebSockets #2907
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -122,14 +122,20 @@ | |
{transport_options, [{max_connections, 1024}, {num_acceptors, 10}]}, | ||
{modules, [ | ||
|
||
{"_", "/http-bind", mod_bosh}, | ||
{"_", "/http-bind", mod_bosh, [ | ||
%% Uncomment to enable HTTP custom headers. Header names must be | ||
%% lowercase. | ||
% {custom_headers, [{"access-control-request-origin", "*"}]} | ||
]}, | ||
{"_", "/ws-xmpp", mod_websockets, [{ejabberd_service, [ | ||
{access, all}, | ||
{shaper_rule, fast}, | ||
{password, "secret"}]} | ||
%% Uncomment to enable connection dropping or/and server-side pings | ||
%{timeout, 600000}, {ping_rate, 2000} | ||
]} | ||
%% Uncomment to enable connection dropping, server-side pings | ||
%% and/or custom HTTP headers. Header names must be lowercase. | ||
% {timeout, 600000}, {ping_rate, 60000}, | ||
% {custom_headers, [{"access-control-Request-origin", "*"}]} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I gotta lowercase this |
||
]} | ||
%% Uncomment to serve static files | ||
%{"_", "/static/[...]", cowboy_static, | ||
% {dir, "/var/www", [{mimetypes, cow_mimetypes, all}]} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,7 +68,8 @@ | |
-type headers_list() :: [{binary(), binary()}]. | ||
|
||
%% Request State | ||
-record(rstate, {req_sid}). | ||
-record(rstate, {req_sid :: undefined | sid(), | ||
custom_headers :: [{binary(), binary()}]}). | ||
-type rstate() :: #rstate{}. | ||
-type req() :: cowboy_req:req(). | ||
|
||
|
@@ -160,12 +161,13 @@ node_cleanup(Acc, Node) -> | |
|
||
-type option() :: {atom(), any()}. | ||
-spec init(req(), _Opts :: [option()]) -> {cowboy_loop, req(), rstate()}. | ||
init(Req, _Opts) -> | ||
init(Req, Opts) -> | ||
?LOG_DEBUG(#{what => bosh_init, req => Req}), | ||
Msg = init_msg(Req), | ||
self() ! Msg, | ||
CustomHeaders = gen_mod:get_opt(custom_headers, Opts, []), | ||
%% Upgrade to cowboy_loop behaviour to enable long polling | ||
{cowboy_loop, Req, #rstate{}}. | ||
{cowboy_loop, Req, #rstate{custom_headers = CustomHeaders}}. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can use |
||
|
||
|
||
%% ok return keep the handler looping. | ||
|
@@ -176,15 +178,15 @@ info(accept_options, Req, State) -> | |
Headers = ac_all(Origin), | ||
?LOG_DEBUG(#{what => bosh_accept_options, headers => Headers, | ||
text => <<"Handle OPTIONS request in Bosh">>}), | ||
Req1 = cowboy_reply(200, Headers, <<>>, Req), | ||
Req1 = cowboy_reply(200, Headers, <<>>, Req, State), | ||
{stop, Req1, State}; | ||
info(accept_get, Req, State) -> | ||
Headers = [content_type(), | ||
ac_allow_methods(), | ||
ac_allow_headers(), | ||
ac_max_age()], | ||
Body = <<"MongooseIM bosh endpoint">>, | ||
Req1 = cowboy_reply(200, Headers, Body, Req), | ||
Req1 = cowboy_reply(200, Headers, Body, Req, State), | ||
{stop, Req1, State}; | ||
info(forward_body, Req, S) -> | ||
{ok, Body, Req1} = cowboy_req:read_body(Req), | ||
|
@@ -200,27 +202,27 @@ info({bosh_reply, El}, Req, S) -> | |
?LOG_DEBUG(#{what => bosh_send, req_sid => S#rstate.req_sid, reply_body => BEl, | ||
sid => exml_query:attr(El, <<"sid">>, <<"missing">>)}), | ||
Headers = bosh_reply_headers(), | ||
Req1 = cowboy_reply(200, Headers, BEl, Req), | ||
Req1 = cowboy_reply(200, Headers, BEl, Req, S), | ||
{stop, Req1, S}; | ||
|
||
info({close, Sid}, Req, S) -> | ||
?LOG_DEBUG(#{what => bosh_close, sid => Sid}), | ||
Req1 = cowboy_reply(200, [], <<>>, Req), | ||
Req1 = cowboy_reply(200, [], <<>>, Req, S), | ||
{stop, Req1, S}; | ||
info(no_body, Req, State) -> | ||
?LOG_DEBUG(#{what => bosh_stop, reason => missing_request_body, req => Req}), | ||
Req1 = no_body_error(Req), | ||
Req1 = no_body_error(Req, State), | ||
{stop, Req1, State}; | ||
info({wrong_method, Method}, Req, State) -> | ||
?LOG_DEBUG(#{what => bosh_stop, reason => wrong_request_method, | ||
method => Method, req => Req}), | ||
Req1 = method_not_allowed_error(Req), | ||
Req1 = method_not_allowed_error(Req, State), | ||
{stop, Req1, State}; | ||
info(item_not_found, Req, S) -> | ||
Req1 = terminal_condition(<<"item-not-found">>, Req), | ||
Req1 = terminal_condition(<<"item-not-found">>, Req, S), | ||
{stop, Req1, S}; | ||
info(policy_violation, Req, S) -> | ||
Req1 = terminal_condition(<<"policy-violation">>, Req), | ||
Req1 = terminal_condition(<<"policy-violation">>, Req, S), | ||
{stop, Req1, S}. | ||
|
||
|
||
|
@@ -300,7 +302,7 @@ forward_body(Req, #xmlel{} = Body, S) -> | |
Type = to_event_type(Body), | ||
case Type of | ||
streamstart -> | ||
{SessionStarted, Req1} = maybe_start_session(Req, Body), | ||
{SessionStarted, Req1} = maybe_start_session(Req, Body, S), | ||
case SessionStarted of | ||
true -> | ||
{ok, Req1, S}; | ||
|
@@ -317,7 +319,7 @@ forward_body(Req, #xmlel{} = Body, S) -> | |
{error, item_not_found} -> | ||
?LOG_WARNING(#{what => bosh_stop, reason => session_not_found, | ||
sid => Sid}), | ||
Req1 = terminal_condition(<<"item-not-found">>, Req), | ||
Req1 = terminal_condition(<<"item-not-found">>, Req, S), | ||
{stop, Req1, S} | ||
end | ||
end. | ||
|
@@ -338,27 +340,27 @@ get_session_socket(Sid) -> | |
end. | ||
|
||
|
||
-spec maybe_start_session(req(), exml:element()) -> | ||
-spec maybe_start_session(req(), exml:element(), rstate()) -> | ||
{SessionStarted :: boolean(), req()}. | ||
maybe_start_session(Req, Body) -> | ||
maybe_start_session(Req, Body, State) -> | ||
Host = exml_query:attr(Body, <<"to">>), | ||
case is_known_host(Host) of | ||
true -> | ||
maybe_start_session_on_known_host(Req, Body); | ||
maybe_start_session_on_known_host(Req, Body, State); | ||
false -> | ||
Req1 = terminal_condition(<<"host-unknown">>, Req), | ||
Req1 = terminal_condition(<<"host-unknown">>, Req, State), | ||
{false, Req1} | ||
end. | ||
|
||
maybe_start_session_on_known_host(Req, Body) -> | ||
maybe_start_session_on_known_host(Req, Body, State) -> | ||
try | ||
maybe_start_session_on_known_host_unsafe(Req, Body) | ||
catch | ||
error:Reason -> | ||
%% It's here because something catch-y was here before | ||
?LOG_ERROR(#{what => bosh_stop, issue => undefined_condition, | ||
reason => Reason}), | ||
Req1 = terminal_condition(<<"undefined-condition">>, [], Req), | ||
Req1 = terminal_condition(<<"undefined-condition">>, [], Req, State), | ||
{false, Req1} | ||
end. | ||
|
||
|
@@ -396,32 +398,32 @@ make_sid() -> | |
%% HTTP errors | ||
%%-------------------------------------------------------------------- | ||
|
||
-spec no_body_error(cowboy_req:req()) -> cowboy_req:req(). | ||
no_body_error(Req) -> | ||
-spec no_body_error(cowboy_req:req(), rstate()) -> cowboy_req:req(). | ||
no_body_error(Req, State) -> | ||
cowboy_reply(400, ac_all(?DEFAULT_ALLOW_ORIGIN), | ||
<<"Missing request body">>, Req). | ||
<<"Missing request body">>, Req, State). | ||
|
||
|
||
-spec method_not_allowed_error(cowboy_req:req()) -> cowboy_req:req(). | ||
method_not_allowed_error(Req) -> | ||
-spec method_not_allowed_error(cowboy_req:req(), rstate()) -> cowboy_req:req(). | ||
method_not_allowed_error(Req, State) -> | ||
cowboy_reply(405, ac_all(?DEFAULT_ALLOW_ORIGIN), | ||
<<"Use POST request method">>, Req). | ||
<<"Use POST request method">>, Req, State). | ||
|
||
%%-------------------------------------------------------------------- | ||
%% BOSH Terminal Binding Error Conditions | ||
%%-------------------------------------------------------------------- | ||
|
||
-spec terminal_condition(binary(), cowboy_req:req()) -> cowboy_req:req(). | ||
terminal_condition(Condition, Req) -> | ||
terminal_condition(Condition, [], Req). | ||
-spec terminal_condition(binary(), cowboy_req:req(), rstate()) -> cowboy_req:req(). | ||
terminal_condition(Condition, Req, State) -> | ||
terminal_condition(Condition, [], Req, State). | ||
|
||
|
||
-spec terminal_condition(binary(), [exml:element()], cowboy_req:req()) | ||
-spec terminal_condition(binary(), [exml:element()], cowboy_req:req(), rstate()) | ||
-> cowboy_req:req(). | ||
terminal_condition(Condition, Details, Req) -> | ||
terminal_condition(Condition, Details, Req, State) -> | ||
Body = terminal_condition_body(Condition, Details), | ||
Headers = [content_type()] ++ ac_all(?DEFAULT_ALLOW_ORIGIN), | ||
cowboy_reply(200, Headers, Body, Req). | ||
cowboy_reply(200, Headers, Body, Req, State). | ||
|
||
|
||
-spec terminal_condition_body(binary(), [exml:element()]) -> binary(). | ||
|
@@ -481,9 +483,10 @@ maybe_set_max_hold(ClientHold, #xmlel{attrs = Attrs} = Body) when ClientHold > 1 | |
maybe_set_max_hold(_, _) -> | ||
{error, invalid_hold}. | ||
|
||
-spec cowboy_reply(non_neg_integer(), headers_list(), binary(), req()) -> req(). | ||
cowboy_reply(Code, Headers, Body, Req) when is_list(Headers) -> | ||
cowboy_req:reply(Code, maps:from_list(Headers), Body, Req). | ||
-spec cowboy_reply(non_neg_integer(), headers_list(), binary(), req(), rstate()) -> req(). | ||
cowboy_reply(Code, Headers, Body, Req, State) when is_list(Headers) -> | ||
CustomHeaders = State#rstate.custom_headers, | ||
cowboy_req:reply(Code, maps:from_list(Headers ++ CustomHeaders), Body, Req). | ||
|
||
config_metrics(Host) -> | ||
OptsToReport = [{backend, mnesia}], %list of tuples {option, defualt_value} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
-module(mod_bosh_SUITE). | ||
-compile([export_all]). | ||
-include_lib("eunit/include/eunit.hrl"). | ||
-define(PORT, 5280). | ||
-define(IP, {127, 0, 0, 1}). | ||
-define(CUSTOM_HEADERS, | ||
[{<<"strict-transport-security">>, <<"max-age=31536000; includeSubDomains">>}, | ||
{<<"access-control-allow-origin">>, <<"*">>}]). | ||
-define(BOSH_BIND_PATH, "/http-bind"). | ||
-define(HANDSHAKE_TIMEOUT, 3000). | ||
-define(eq(E, I), ?assertEqual(E, I)). | ||
-define(FAST_PING_RATE, 500). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some of these defines can be removed. I suspect it's a direct copy & paste from websockets suite? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is indeed |
||
-define(NEW_TIMEOUT, 1200). | ||
%The timeout is long enough to pass all test cases for ping interval settings | ||
%using NEW_TIMEOUT value. In these tests we wait for at most 2 pings. | ||
%The 300ms is just an additional overhead | ||
-define(IDLE_TIMEOUT, ?NEW_TIMEOUT * 2 + 300). | ||
|
||
all() -> | ||
custom_headers_tests(). | ||
|
||
custom_headers_tests() -> | ||
[verify_custom_headers]. | ||
|
||
init_per_suite(C) -> | ||
setup(), | ||
C. | ||
|
||
end_per_suite(_) -> | ||
teardown(), | ||
ok. | ||
|
||
init_per_testcase(_, C) -> | ||
C. | ||
|
||
end_per_testcase(_, C) -> | ||
C. | ||
|
||
|
||
setup() -> | ||
meck:unload(), | ||
application:ensure_all_started(cowboy), | ||
application:ensure_all_started(jid), | ||
meck:new(supervisor, [unstick, passthrough, no_link]), | ||
meck:new(gen_mod,[unstick, passthrough, no_link]), | ||
%% Set ping rate | ||
meck:expect(supervisor, start_child, | ||
fun(ejabberd_listeners, {_, {_, start_link, [_]}, transient, | ||
infinity, worker, [_]}) -> {ok, self()}; | ||
(A,B) -> meck:passthrough([A,B]) | ||
end), | ||
meck:expect(ejabberd_config, get_local_option, fun(_) -> undefined end), | ||
%% Start websocket cowboy listening | ||
|
||
Opts = [{num_acceptors, 10}, | ||
{max_connections, 1024}, | ||
{modules, [{"_", "/http-bind", mod_bosh, [{custom_headers, ?CUSTOM_HEADERS}]}]}], | ||
ejabberd_cowboy:start_listener({?PORT, ?IP, tcp}, Opts). | ||
|
||
teardown() -> | ||
meck:unload(), | ||
cowboy:stop_listener(ejabberd_cowboy:ref({?PORT, ?IP, tcp})), | ||
application:stop(cowboy), | ||
%% Do not stop jid, Erlang 21 does not like to reload nifs | ||
ok. | ||
|
||
verify_custom_headers(_Config) -> | ||
application:ensure_all_started(gun), | ||
{ok, ConnPid} = gun:open("127.0.0.1", ?PORT), | ||
{ok, http} = gun:await_up(ConnPid), | ||
StreamRef = gun:get(ConnPid, ?BOSH_BIND_PATH), | ||
case gun:await(ConnPid, StreamRef) of | ||
{response, fin, _Status, _Headers} -> | ||
ct:fail(no_bosh_http_response); | ||
{response, nofin, 200, Headers} -> | ||
{ok, _Body} = gun:await_body(ConnPid, StreamRef), | ||
RcvdHdrsSet = sets:from_list(Headers), | ||
CustomHdrsSet = sets:from_list(?CUSTOM_HEADERS), | ||
?assert(sets:is_subset(CustomHdrsSet, RcvdHdrsSet)) | ||
end, | ||
application:stop(gun). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to be rewritten for TOML
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any thoughts on how you want it written for TOML? The precedent in
mod_revproxy
is tuple list of headers. I cannot tell if you're gonna move that mod to TOML or drop it.Since Cowboy natively uses maps HTTP headers, I'd like to map the headers as a map in the TOML. Will that work, even if I stray from the extra headers convention in
mod_revproxy
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole
mod_revproxy
functionality is deprecated and will be dropped in the next release unless a new demand appears (which is unlikely). Custom headers would need to be added to TOML as a table (which is parsed as a map bytomerl
). However, the whole TOML parser is being rewritten at the moment and a part of this code would need to be rewritten, unless you merge this PR to thetoml-config
branch.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll wait until #2929 is merged. I don't think it'd make sense to pollute that branch with this change, which is more functionality than config. I also plan on adding some default security headers like:
to the HTTPS endpoints as part of the standard as part of the standard
vars-tool.config.in
.