%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2010-2011. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
%%
%% Tests of traffic between six Diameter nodes connected as follows.
%%
%% ---- SERVER.REALM1 (TLS after capabilities exchange)
%% /
%% / ---- SERVER.REALM2 (ditto)
%% | /
%% CLIENT.REALM0 ----- SERVER.REALM3 (no security)
%% | \
%% \ ---- SERVER.REALM4 (TLS at connection establishment)
%% \
%% ---- SERVER.REALM5 (ditto)
%%
-module(diameter_tls_SUITE).
-export([suite/0,
all/0,
groups/0,
init_per_group/2,
end_per_group/2,
init_per_suite/1,
end_per_suite/1]).
%% testcases
-export([send1/1,
send2/1,
send3/1,
send4/1,
send5/1,
remove_transports/1,
stop_services/1]).
%% diameter callbacks
-export([peer_up/3,
peer_down/3,
pick_peer/4,
prepare_request/3,
prepare_retransmit/3,
handle_answer/4,
handle_error/4,
handle_request/3]).
-ifdef(DIAMETER_CT).
-include("diameter_gen_base_rfc3588.hrl").
-else.
-include_lib("diameter/include/diameter_gen_base_rfc3588.hrl").
-endif.
-include_lib("diameter/include/diameter.hrl").
-include("diameter_ct.hrl").
%% ===========================================================================
-define(ADDR, {127,0,0,1}).
-define(CLIENT, "CLIENT.REALM0").
-define(SERVER1, "SERVER.REALM1").
-define(SERVER2, "SERVER.REALM2").
-define(SERVER3, "SERVER.REALM3").
-define(SERVER4, "SERVER.REALM4").
-define(SERVER5, "SERVER.REALM5").
-define(SERVERS, [?SERVER1, ?SERVER2, ?SERVER3, ?SERVER4, ?SERVER5]).
-define(DICT_COMMON, ?DIAMETER_DICT_COMMON).
-define(APP_ALIAS, the_app).
-define(APP_ID, ?DICT_COMMON:id()).
-define(NO_INBAND_SECURITY, 0).
-define(TLS, 1).
%% Config for diameter:start_service/2.
-define(SERVICE(Host, Dict),
[{'Origin-Host', Host},
{'Origin-Realm', realm(Host)},
{'Host-IP-Address', [?ADDR]},
{'Vendor-Id', 12345},
{'Product-Name', "OTP/diameter"},
{'Inband-Security-Id', [?NO_INBAND_SECURITY]},
{'Auth-Application-Id', [Dict:id()]},
{application, [{alias, ?APP_ALIAS},
{dictionary, Dict},
{module, ?MODULE},
{answer_errors, callback}]}]).
%% Config for diameter:add_transport/2. In the listening case, listen
%% on a free port that we then lookup using the implementation detail
%% that diameter_tcp registers the port with diameter_reg.
-define(CONNECT(PortNr, Caps, Opts),
{connect, [{transport_module, diameter_tcp},
{transport_config, [{raddr, ?ADDR},
{rport, PortNr},
{ip, ?ADDR},
{port, 0}
| Opts]},
{capabilities, Caps}]}).
-define(LISTEN(Caps, Opts),
{listen, [{transport_module, diameter_tcp},
{transport_config, [{ip, ?ADDR}, {port, 0} | Opts]},
{capabilities, Caps}]}).
-define(SUCCESS, 2001).
-define(LOGOUT, ?'DIAMETER_BASE_TERMINATION-CAUSE_DIAMETER_LOGOUT').
%% ===========================================================================
suite() ->
[{timetrap, {seconds, 15}}].
all() ->
[{group, N} || {N, _, _} <- groups()]
++ [remove_transports, stop_services].
groups() ->
Ts = tc(),
[{all, [], Ts},
{p, [parallel], Ts}].
init_per_group(_, Config) ->
Config.
end_per_group(_, _) ->
ok.
init_per_suite(Config) ->
init(os:find_executable("openssl"), Config).
init(false, _) ->
{skip, no_openssl};
init(_, Config) ->
ok = ssl:start(),
ok = diameter:start(),
Dir = proplists:get_value(priv_dir, Config),
Servers = [server(S, sopts(S, Dir)) || S <- ?SERVERS],
ok = diameter:start_service(?CLIENT, ?SERVICE(?CLIENT, ?DICT_COMMON)),
true = diameter:subscribe(?CLIENT),
Opts = ssl_options(Dir, "client"),
Connections = [connect(?CLIENT, S, copts(N, Opts))
|| {S,N} <- lists:zip(Servers, ?SERVERS)],
[{transports, lists:zip(Servers, Connections)} | Config].
end_per_suite(_Config) ->
ok = diameter:stop(),
ok = ssl:stop().
%% Testcases to run when services are started and connections
%% established. These are trivial, the interesting stuff is setting up
%% the connections in init_per_suite/2.
tc() ->
[send1,
send2,
send3,
send4,
send5].
%% ===========================================================================
%% testcases
%% Send an STR intended for a specific server and expect success.
send1(_Config) ->
call(?SERVER1).
send2(_Config) ->
call(?SERVER2).
send3(_Config) ->
call(?SERVER3).
send4(_Config) ->
call(?SERVER4).
send5(_Config) ->
call(?SERVER5).
%% Remove the client transports and expect the corresponding server
%% transport to go down.
remove_transports(Config) ->
Ts = proplists:get_value(transports, Config),
[] = [T || S <- ?SERVERS, T <- [diameter:subscribe(S)], T /= true],
lists:map(fun disconnect/1, Ts).
stop_services(_Config) ->
Hs = [?CLIENT | ?SERVERS],
Ok = [ok || _ <- Hs],
Ok = [diameter:stop_service(H) || H <- Hs].
%% ===========================================================================
%% diameter callbacks
%% peer_up/3
peer_up(_SvcName, _Peer, State) ->
State.
%% peer_down/3
peer_down(_SvcName, _Peer, State) ->
State.
%% pick_peer/4
pick_peer([Peer], _, ?CLIENT, _State) ->
{ok, Peer}.
%% prepare_request/3
prepare_request(#diameter_packet{msg = Req},
?CLIENT,
{_Ref, Caps}) ->
#diameter_caps{origin_host = {OH, _},
origin_realm = {OR, _}}
= Caps,
{send, set(Req, [{'Session-Id', diameter:session_id(OH)},
{'Origin-Host', OH},
{'Origin-Realm', OR}])}.
%% prepare_retransmit/3
prepare_retransmit(_Pkt, false, _Peer) ->
discard.
%% handle_answer/4
handle_answer(Pkt, _Req, ?CLIENT, _Peer) ->
#diameter_packet{msg = Rec, errors = []} = Pkt,
Rec.
%% handle_error/4
handle_error(Reason, _Req, ?CLIENT, _Peer) ->
{error, Reason}.
%% handle_request/3
handle_request(#diameter_packet{msg = #diameter_base_STR{'Session-Id' = SId}},
OH,
{_Ref, #diameter_caps{origin_host = {OH,_},
origin_realm = {OR, _}}})
when OH /= ?CLIENT ->
{reply, #diameter_base_STA{'Result-Code' = ?SUCCESS,
'Session-Id' = SId,
'Origin-Host' = OH,
'Origin-Realm' = OR}}.
%% ===========================================================================
%% support functions
call(Server) ->
Realm = realm(Server),
Req = ['STR', {'Destination-Realm', Realm},
{'Termination-Cause', ?LOGOUT},
{'Auth-Application-Id', ?APP_ID}],
#diameter_base_STA{'Result-Code' = ?SUCCESS,
'Origin-Host' = Server,
'Origin-Realm' = Realm}
= call(Req, [{filter, realm}]).
call(Req, Opts) ->
diameter:call(?CLIENT, ?APP_ALIAS, Req, Opts).
set([H|T], Vs) ->
[H | Vs ++ T].
disconnect({{LRef, _PortNr}, CRef}) ->
ok = diameter:remove_transport(?CLIENT, CRef),
ok = receive #diameter_event{info = {down, LRef, _, _}} -> ok
after 2000 -> false
end.
realm(Host) ->
tl(lists:dropwhile(fun(C) -> C /= $. end, Host)).
inband_security(Ids) ->
[{'Inband-Security-Id', Ids}].
ssl_options(Dir, Base) ->
{Key, Cert} = make_cert(Dir, Base ++ "_key.pem", Base ++ "_ca.pem"),
[{ssl_options, [{certfile, Cert}, {keyfile, Key}]}].
make_cert(Dir, Keyfile, Certfile) ->
[K,C] = Paths = [filename:join([Dir, F]) || F <- [Keyfile, Certfile]],
KCmd = join(["openssl genrsa -out", K, "2048"]),
CCmd = join(["openssl req -new -x509 -key", K, "-out", C, "-days 7",
"-subj /C=SE/ST=./L=Stockholm/CN=www.erlang.org"]),
%% Hope for the best and only check that files are written.
os:cmd(KCmd),
os:cmd(CCmd),
[_,_] = [T || P <- Paths, {ok, T} <- [file:read_file_info(P)]],
{K,C}.
join(Strs) ->
string:join(Strs, " ").
%% server/2
server(Host, {Caps, Opts}) ->
ok = diameter:start_service(Host, ?SERVICE(Host, ?DICT_COMMON)),
{ok, LRef} = diameter:add_transport(Host, ?LISTEN(Caps, Opts)),
{LRef, portnr(LRef)}.
sopts(?SERVER1, Dir) ->
{inband_security([?TLS]),
ssl_options(Dir, "server1")};
sopts(?SERVER2, Dir) ->
{inband_security([?NO_INBAND_SECURITY, ?TLS]),
ssl_options(Dir, "server2")};
sopts(?SERVER3, _) ->
{[], []};
sopts(?SERVER4, Dir) ->
{[], ssl(ssl_options(Dir, "server4"))};
sopts(?SERVER5, Dir) ->
{[], ssl(ssl_options(Dir, "server5"))}.
ssl([{ssl_options = T, Opts}]) ->
[{T, true} | Opts].
portnr(LRef) ->
portnr(LRef, 20).
portnr(LRef, N)
when 0 < N ->
case diameter_reg:match({diameter_tcp, listener, {LRef, '_'}}) of
[{T, _Pid}] ->
{_, _, {LRef, {_Addr, LSock}}} = T,
{ok, PortNr} = to_portnr(LSock) ,
PortNr;
[] ->
receive after 500 -> ok end,
portnr(LRef, N-1)
end.
to_portnr(Sock)
when is_port(Sock) ->
inet:port(Sock);
to_portnr(Sock) ->
case ssl:sockname(Sock) of
{ok, {_,N}} ->
{ok, N};
No ->
No
end.
%% connect/3
connect(Host, {_LRef, PortNr}, {Caps, Opts}) ->
{ok, Ref} = diameter:add_transport(Host, ?CONNECT(PortNr, Caps, Opts)),
ok = receive
#diameter_event{service = Host,
info = {up, Ref, _, _, #diameter_packet{}}} ->
ok
after 2000 ->
false
end,
Ref.
copts(S, Opts)
when S == ?SERVER1;
S == ?SERVER2;
S == ?SERVER3 ->
{inband_security([?NO_INBAND_SECURITY, ?TLS]), Opts};
copts(S, Opts)
when S == ?SERVER4;
S == ?SERVER5 ->
{[], ssl(Opts)}.