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 option to modify Cowboy server name returned in headers #2308

Merged
merged 4 commits into from
May 29, 2019
Merged
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
61 changes: 46 additions & 15 deletions big_tests/tests/rest_client_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,46 @@
-include_lib("common_test/include/ct.hrl").

-import(rest_helper,
[assert_inlist/2,
assert_notinlist/2,
decode_maplist/1,
gett/2,
[decode_maplist/1,
gett/3,
post/3,
post/4,
putt/3,
putt/4,
delete/2,
delete/3,
delete/4]
).

-import(muc_light_helper,
[set_mod_config/3]).

-import(escalus_ejabberd, [rpc/3]).

-define(PRT(X, Y), ct:pal("~p: ~p", [X, Y])).
-define(OK, {<<"200">>, <<"OK">>}).
-define(CREATED, {<<"201">>, <<"Created">>}).
-define(NOCONTENT, {<<"204">>, <<"No Content">>}).
-define(ERROR, {<<"500">>, _}).
-define(NOT_FOUND, {<<"404">>, _}).
-define(NOT_IMPLEMENTED, {<<"501">>, _}).
-define(UNAUTHORIZED, {<<"401">>, <<"Unauthorized">>}).
-define(MUCHOST, <<"muclight.localhost">>).

%% --------------------------------------------------------------------
%% Common Test stuff
%% --------------------------------------------------------------------

all() ->
[{group, messages},
{group, muc},
{group, muc_config},
{group, roster},
{group, messages_with_props}].
{group, messages_with_props},
{group, security}].

groups() ->
G = [{messages_with_props, [parallel], message_with_props_test_cases()},
{messages, [parallel], message_test_cases()},
{muc, [pararell], muc_test_cases()},
{muc_config, [], muc_config_cases()},
{roster, [parallel], roster_test_cases()}],
{roster, [parallel], roster_test_cases()},
{security, [], security_test_cases()}],
ct_helper:repeat_all_until_all_ok(G).

message_test_cases() ->
Expand Down Expand Up @@ -106,6 +105,12 @@ message_with_props_test_cases() ->
msg_with_malformed_props_is_sent_and_delivered_over_xmpp
].

security_test_cases() ->
[
default_http_server_name_is_returned_if_not_changed,
non_default_http_server_name_is_returned_if_configured
].

init_per_suite(C) ->
application:ensure_all_started(shotgun),
Host = ct:get_config({hosts, mim, domain}),
Expand Down Expand Up @@ -158,6 +163,10 @@ end_per_testcase(config_can_be_changed_by_all = CaseName, Config) ->
end_per_testcase(TC, C) ->
escalus:end_per_testcase(TC, C).

%% --------------------------------------------------------------------
%% Test cases
%% --------------------------------------------------------------------

msg_is_sent_and_delivered_over_xmpp(Config) ->
escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
M = send_message(alice, Alice, Bob),
Expand Down Expand Up @@ -581,7 +590,6 @@ msg_with_malformed_props_can_be_parsed(Config) ->

end).


assert_room_messages(RecvMsg, {_ID, _GenFrom, GenMsg}) ->
escalus:assert(is_chat_message, [maps:get(body, RecvMsg)], GenMsg),
ok.
Expand Down Expand Up @@ -652,7 +660,7 @@ when_config_change(User, RoomID, NewName, NewSubject) ->
Creds = credentials(User),
Config = #{name => NewName, subject => NewSubject},
Path = <<"/rooms/", RoomID/binary, "/config">>,
rest_helper:putt(client, Path, Config, Creds).
putt(client, Path, Config, Creds).

maybe_wait_for_aff_stanza(#client{} = Client, Invitee) ->
Stanza = escalus:wait_for_stanza(Client),
Expand Down Expand Up @@ -690,7 +698,7 @@ send_message(User, From, To) ->
BobJID = user_jid(To),
M = #{to => BobJID, body => <<"hello, ", BobJID/binary, " it's me">>},
Cred = credentials({User, From}),
{{<<"200">>, <<"OK">>}, {Result}} = rest_helper:post(client, <<"/messages">>, M, Cred),
{{<<"200">>, <<"OK">>}, {Result}} = post(client, <<"/messages">>, M, Cred),
ID = proplists:get_value(<<"id">>, Result),
M#{id => ID, from => AliceJID}.

Expand Down Expand Up @@ -762,7 +770,7 @@ create_room_with_id_request(Creds, RoomName, Subject, RoomID) ->
Room = #{name => RoomName,
subject => Subject},
Path = <<"/rooms/", RoomID/binary>>,
rest_helper:putt(client, Path, Room, Creds).
putt(client, Path, Room, Creds).

get_my_rooms(User) ->
Creds = credentials(User),
Expand Down Expand Up @@ -1163,3 +1171,26 @@ room_jid(RoomID, Config) ->

domain(Config) ->
?config(muc_light_host, Config).

default_http_server_name_is_returned_if_not_changed(_Config) ->
%% GIVEN MIM1 uses default name
verify_server_name_in_header(distributed_helper:mim(), <<"Cowboy">>).

non_default_http_server_name_is_returned_if_configured(_Config) ->
%% GIVEN MIM2 uses name "Classified"
verify_server_name_in_header(distributed_helper:mim2(), <<"Classified">>).

verify_server_name_in_header(Server, ExpectedName) ->
% WHEN unathenticated user makes a request to nonexistent path
ReqParams = #{
role => client,
method => <<"GET">>,
path => "/contacts/zorro@localhost",
body => <<>>,
return_headers => true,
server => Server
},
{?UNAUTHORIZED, Headers2, _} = rest_helper:make_request(ReqParams),
% THEN expected server name is returned
ExpectedName = proplists:get_value(<<"server">>, Headers2).

120 changes: 77 additions & 43 deletions big_tests/tests/rest_helper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
delete/2,
delete/3,
delete/4,
make_request/1,
maybe_enable_mam/3,
maybe_disable_mam/2,
maybe_skip_mam_test_cases/3,
Expand All @@ -32,6 +33,15 @@
-define(PATHPREFIX, <<"/api">>).

-type role() :: admin | client.
-type credentials() :: {Username :: binary(), Password :: binary()}.
-type request_params() :: #{
role := role(),
method := binary(),
creds => credentials(),
path := binary(),
body := binary(),
return_headers := boolean(),
server := atom() }.

%%--------------------------------------------------------------------
%% Helpers
Expand Down Expand Up @@ -87,51 +97,66 @@ assert_notinmaplist([K|Keys], Map, L, Orig) ->


gett(Role, Path) ->
make_request(Role, <<"GET">>, Path).
make_request(#{ role => Role, method => <<"GET">>, path => Path }).

post(Role, Path, Body) ->
make_request(Role, <<"POST">>, Path, Body).
make_request(#{ role => Role, method => <<"POST">>, path => Path, body => Body }).

putt(Role, Path, Body) ->
make_request(Role, <<"PUT">>, Path, Body).
make_request(#{ role => Role, method => <<"PUT">>, path => Path, body => Body }).

delete(Role, Path) ->
make_request(Role, <<"DELETE">>, Path).
make_request(#{ role => Role, method => <<"DELETE">>, path => Path }).

-spec gett(role(), Path :: string()|binary(), Cred :: {Username :: binary(), Password :: binary()}) -> term().
gett(Role, Path, Cred) ->
make_request(Role, {<<"GET">>, Cred}, Path).
make_request(#{ role => Role, method => <<"GET">>, creds => Cred, path => Path}).

post(Role, Path, Body, Cred) ->
make_request(Role, {<<"POST">>, Cred}, Path, Body).
make_request(#{ role => Role, method => <<"POST">>, creds => Cred, path => Path, body => Body }).

putt(Role, Path, Body, Cred) ->
make_request(Role, {<<"PUT">>, Cred}, Path, Body).
make_request(#{ role => Role, method => <<"PUT">>, creds => Cred, path => Path, body => Body }).

delete(Role, Path, Cred) ->
make_request(Role, {<<"DELETE">>, Cred}, Path).
make_request(#{ role => Role, method => <<"DELETE">>, creds => Cred, path => Path }).

delete(Role, Path, Cred, Body) ->
make_request(Role, {<<"DELETE">>, Cred}, Path, Body).



make_request(Role, Method, Path) ->
make_request(Role, Method, Path, <<"">>).

make_request(Role, Method, Path, ReqBody) when is_map(ReqBody) ->
make_request(Role, Method, Path, jiffy:encode(ReqBody));
make_request(Role, Method, Path, ReqBody) when not is_binary(Path) ->
make_request(Role, Method, list_to_binary(Path), ReqBody);
make_request(Role, Method, Path, ReqBody) ->
CPath = <<?PATHPREFIX/binary, Path/binary>>,
{Code, RespBody} = case fusco_request(Role, Method, CPath, ReqBody) of
{RCode, _, Body, _, _} ->
{RCode, Body};
{RCode, _, Body, _, _, _} ->
{RCode, Body}
end,
{Code, decode(RespBody)}.
make_request(#{ role => Role, method => <<"DELETE">>, creds => Cred, path => Path, body => Body }).

-spec make_request(request_params()) ->
{{Number :: binary(), Text :: binary()},
Headers :: [{binary(), binary()}],
Body :: map() | binary()}.
make_request(#{ return_headers := true } = Params) ->
NormalizedParams = normalize_path(normalize_body(fill_default_server(Params))),
case fusco_request(NormalizedParams) of
{RCode, RHeaders, Body, _, _} ->
{RCode, normalize_headers(RHeaders), decode(Body)};
{RCode, RHeaders, Body, _, _, _} ->
{RCode, normalize_headers(RHeaders), decode(Body)}
end;
make_request(#{ return_headers := false } = Params) ->
{Code, _, Body} = make_request(Params#{ return_headers := true }),
{Code, Body};
make_request(Params) ->
make_request(Params#{ return_headers => false }).

normalize_path(#{ path := Path } = Params) when not is_binary(Path) ->
normalize_path(Params#{ path := list_to_binary(Path) });
normalize_path(#{ path := Path } = Params) ->
Params#{ path := <<?PATHPREFIX/binary, Path/binary>> }.

normalize_body(#{ body := Body } = Params) when is_map(Body) ->
Params#{ body := jiffy:encode(Body) };
normalize_body(#{ body := Body } = Params) when is_binary(Body) ->
Params;
normalize_body(Params) ->
Params#{ body => <<>> }.

fill_default_server(#{ server := _Server } = Params) ->
Params;
fill_default_server(Params) ->
Params#{ server => mim() }.

decode(<<>>) ->
<<"">>;
Expand All @@ -143,14 +168,21 @@ decode(RespBody) ->
RespBody
end.

normalize_headers(Headers) ->
lists:map(fun({K, V}) when is_binary(V) -> {K, V};
({K, V}) when is_list(V) -> {K, iolist_to_binary(V)} end, Headers).

%% a request specyfying credentials is directed to client http listener
fusco_request(Role, {Method, {User, Password}}, Path, Body) ->
Basic = list_to_binary("Basic " ++ base64:encode_to_string(to_list(User) ++ ":"++ to_list(Password))),
fusco_request(#{ role := Role, method := Method, creds := {User, Password},
path := Path, body := Body, server := Server }) ->
EncodedAuth = base64:encode_to_string(to_list(User) ++ ":"++ to_list(Password)),
Basic = list_to_binary("Basic " ++ EncodedAuth),
Headers = [{<<"authorization">>, Basic}],
fusco_request(Method, Path, Body, Headers, get_port(Role), get_ssl_status(Role));
fusco_request(Method, Path, Body, Headers,
get_port(Role, Server), get_ssl_status(Role, Server));
%% without them it is for admin (secure) interface
fusco_request(Role, Method, Path, Body) ->
fusco_request(Method, Path, Body, [], get_port(Role), get_ssl_status(Role)).
fusco_request(#{ role := Role, method := Method, path := Path, body := Body, server := Server }) ->
fusco_request(Method, Path, Body, [], get_port(Role, Server), get_ssl_status(Role, Server)).

fusco_request(Method, Path, Body, HeadersIn, Port, SSL) ->
{ok, Client} = fusco_cp:start_link({"localhost", Port, SSL}, [], 1),
Expand All @@ -159,20 +191,22 @@ fusco_request(Method, Path, Body, HeadersIn, Port, SSL) ->
fusco_cp:stop(Client),
Result.

-spec get_port(role()) -> Port :: integer().
get_port(Role) ->
Listeners = rpc(mim(), ejabberd_config, get_local_option, [listen]),
[{PortIpNet, ejabberd_cowboy, _Opts}] = lists:filter(fun(Config) -> is_roles_config(Config, Role) end, Listeners),
-spec get_port(Role :: role(), Server :: atom()) -> Port :: integer().
get_port(Role, Server) ->
Listeners = rpc(Server, ejabberd_config, get_local_option, [listen]),
[{PortIpNet, ejabberd_cowboy, _Opts}] =
lists:filter(fun(Config) -> is_roles_config(Config, Role) end, Listeners),
case PortIpNet of
{Port, _Host, _Net} -> Port;
{Port, _Host} -> Port;
Port -> Port
end.

-spec get_ssl_status(role()) -> boolean().
get_ssl_status(Role) ->
Listeners = rpc(mim(), ejabberd_config, get_local_option, [listen]),
[{_PortIpNet, _Module, Opts}] = lists:filter(fun (Opts) -> is_roles_config(Opts, Role) end, Listeners),
-spec get_ssl_status(Role :: role(), Server :: atom()) -> boolean().
get_ssl_status(Role, Server) ->
Listeners = rpc(Server, ejabberd_config, get_local_option, [listen]),
[{_PortIpNet, _Module, Opts}] =
lists:filter(fun (Opts) -> is_roles_config(Opts, Role) end, Listeners),
lists:keymember(ssl, 1, Opts).

% @doc Changes the control credentials for admin by restarting the listener
Expand Down Expand Up @@ -403,4 +437,4 @@ make_malformed_msg_stanza_with_props(ToJID,MsgID) ->
<value type='long'>1234567890</value>
</property2>
</properties>
</message>">>).
</message>">>).
8 changes: 7 additions & 1 deletion doc/Advanced-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Retaining the default layout is recommended so that the experienced MongooseIM u

## Options

* All options except `hosts`, `host`, `host_config`, `listen` and `outgoing_connections` and can be used in the `host_config` tuple.
* All options except `hosts`, `host`, `host_config`, `listen` and `outgoing_connections` may be used in the `host_config` tuple.

* There are two kinds of local options - those that are kept separately for each domain in the config file (defined inside `host_config`) and the options local for a node in the cluster.

Expand Down Expand Up @@ -351,6 +351,12 @@ There are some additional options that influence all database connections in the
* **Syntax:** `{replaced_wait_timeout, TimeInMilliseconds}`
* **Default:** `2000`

* **cowboy_server_name** (local)
* **Description:** If configured, replaces Cowboy's default name returned in the `server` HTTP response header. It may be used for extra security, as it makes it harder for the malicious user to learn what HTTP software is running under a specific port. This option applies to **all** listeners started by the `ejabberd_cowboy` module.
* **Syntax:** `{cowboy_server_name, NewName}`
* **Default:** no value, i.e. `Cowboy` is used as a header value
* **Example:** `{cowboy_server_name, "Apache"}`

### Modules

For a specific configuration, please refer to [Modules](advanced-configuration/Modules.md) page.
Expand Down
2 changes: 2 additions & 0 deletions rel/files/mongooseim.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,8 @@

{all_metrics_are_global, {{{all_metrics_are_global}}} }.

{{{cowboy_server_name}}}

%%%. ========
%%%' SERVICES

Expand Down
1 change: 1 addition & 0 deletions rel/mim2.vars.config
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
" {password, \"secret\"}\n"
" ]}"}.
{all_metrics_are_global, true}.
{cowboy_server_name, "{cowboy_server_name, \"Classified\"}."}.
{c2s_dhfile, ",{dhfile, \"priv/ssl/fake_dh_server.pem\"}"}.
{s2s_dhfile, ",{dhfile, \"priv/ssl/fake_dh_server.pem\"}"}.

Expand Down
2 changes: 2 additions & 0 deletions src/config/mongoose_config_parser.erl
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ process_term(Term, State) ->
add_option(sasl_mechanisms, Mechanisms, State);
{all_metrics_are_global, Value} ->
add_option(all_metrics_are_global, Value, State);
{cowboy_server_name, Value} ->
add_option(cowboy_server_name, Value, State);
{services, Value} ->
add_option(services, Value, State);
{_Opt, _Val} ->
Expand Down
Loading