%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2010-2012. 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 capabilities exchange between Diameter nodes. In
%% particular, of error and event handling.
%%
-module(diameter_capx_SUITE).
-export([suite/0,
all/0,
groups/0,
init_per_testcase/2,
end_per_testcase/2]).
%% testcases
-export([start/1,
start_services/1,
add_listeners/1,
s_no_common_application/1,
c_no_common_application/1,
s_no_common_security/1,
c_no_common_security/1,
s_unknown_peer/1,
c_unknown_peer/1,
s_unable/1,
c_unable/1,
s_client_reject/1,
c_client_reject/1,
remove_listeners/1,
stop_services/1,
stop/1]).
%% diameter callbacks
-export([peer_up/4,
peer_down/4]).
-include("diameter.hrl").
-include("diameter_gen_base_rfc3588.hrl").
%% ===========================================================================
-define(util, diameter_util).
-define(CLIENT, client).
-define(SERVER, server).
-define(ADDR, {127,0,0,1}).
-define(REALM, "erlang.org").
-define(HOST(Name), Name ++ "." ++ ?REALM).
%% Config for diameter:start_service/2.
-define(SERVICE(Name),
[{'Origin-Realm', ?REALM},
{'Host-IP-Address', [?ADDR]},
{'Vendor-Id', 12345},
{'Product-Name', "OTP/diameter"},
{'Auth-Application-Id', [?DIAMETER_APP_ID_COMMON]},
{'Acct-Application-Id', [?DIAMETER_APP_ID_ACCOUNTING]}
| [{application, [{alias, A},
{dictionary, D},
{module, [?MODULE, A]}]}
|| {A,D} <- [{common, ?DIAMETER_DICT_COMMON},
{accounting, ?DIAMETER_DICT_ACCOUNTING}]]]).
-define(A, list_to_atom).
-define(L, atom_to_list).
-define(event, #diameter_event).
-define(caps, #diameter_caps).
-define(packet, #diameter_packet).
-define(cea, #diameter_base_CEA).
-define(answer_message, #'diameter_base_answer-message').
-define(fail(T), erlang:error({T, process_info(self(), messages)})).
-define(TIMEOUT, 10000).
%% ===========================================================================
suite() ->
[{timetrap, {seconds, 60}}].
all() -> [start,
start_services,
add_listeners,
{group, all},
{group, all, [parallel]},
remove_listeners,
stop_services,
stop].
groups() ->
[{all, [], lists:flatmap(fun tc/1, tc())}].
%% Generate a unique hostname for each testcase so that watchdogs
%% don't prevent a connection from being brought up immediately.
init_per_testcase(Name, Config) ->
Uniq = ["." ++ integer_to_list(N) || N <- tuple_to_list(now())],
[{host, lists:flatten([?L(Name) | Uniq])} | Config].
end_per_testcase(N, _)
when N == start;
N == start_services;
N == add_listeners;
N == remove_listeners;
N == stop_services;
N == stop ->
ok;
end_per_testcase(Name, Config) ->
CRef = ?util:read_priv(Config, Name),
ok = diameter:remove_transport(?CLIENT, CRef).
%% Testcases all come in two flavours, client and server.
tc(Name) ->
[?A([C,$_|?L(Name)]) || C <- "cs"].
tc() ->
[no_common_application,
no_common_security,
unknown_peer,
unable,
client_reject].
%% ===========================================================================
%% start/stop testcases
start(_Config) ->
ok = diameter:start().
start_services(_Config) ->
ok = diameter:start_service(?SERVER, ?SERVICE(?SERVER)),
ok = diameter:start_service(?CLIENT, ?SERVICE(?CLIENT)).
%% One server that responds only to base accounting, one that responds
%% to both this and the common application. Share a common service just
%% to simplify config, and because we can.
add_listeners(Config) ->
Acct = listen(?SERVER,
[{capabilities, [{'Origin-Host', ?HOST("acct-srv")},
{'Auth-Application-Id', []}]},
{applications, [accounting]},
{capabilities_cb, [fun server_capx/3, acct]}]),
Base = listen(?SERVER,
[{capabilities, [{'Origin-Host', ?HOST("base-srv")}]},
{capabilities_cb, [fun server_capx/3, base]}]),
?util:write_priv(Config, ?MODULE, {Base, Acct}). %% lref/2 reads
remove_listeners(_Config) ->
ok = diameter:remove_transport(?SERVER, true).
stop_services(_Config) ->
ok = diameter:stop_service(?CLIENT),
ok = diameter:stop_service(?SERVER).
stop(_Config) ->
ok = diameter:stop().
%% ===========================================================================
%% All the testcases come in pairs, one for receiving an event on the
%% client side, one on the server side. Note that testcases will
%% receive events resulting from other testcases when running in
%% parallel since the events are per service. The unique client
%% Origin-Host for each testcase plus transport references are used to
%% ensure that only the relevant event is extracted from the mailbox.
%% Don't bother extracting events that aren't relevant.
%% ====================
%% Ask the accounting server to speak the common application and expect
%% DIAMETER_NO_COMMON_APPLICATION = 5010.
s_no_common_application(Config) ->
server_closed(Config, fun no_common_application/1, 5010).
c_no_common_application(Config) ->
client_closed(Config, "acct-srv", fun no_common_application/1, 5010).
no_common_application(Config) ->
connect(Config, acct, [{capabilities, [{'Acct-Application-Id', []}]},
{applications, [common]}]).
%% ====================
%% Ask the base server to speak accounting with an unknown security
%% method and expect DIAMETER_NO_COMMON_SECURITY = 5017.
s_no_common_security(Config) ->
server_closed(Config, fun no_common_security/1, 5017).
c_no_common_security(Config) ->
client_closed(Config, "base-srv", fun no_common_security/1, 5017).
no_common_security(Config) ->
connect(Config, base, [{capabilities, [{'Acct-Application-Id', []},
{'Inband-Security-Id', [17, 18]}]},
{applications, [common]}]).
%% ====================
%% Have the base server reject a decent CER with the protocol error
%% DIAMETER_UNKNOWN_PEER = 3010.
s_unknown_peer(Config) ->
server_reject(Config, fun base/1, 3010).
c_unknown_peer(Config) ->
true = diameter:subscribe(?CLIENT),
OH = ?HOST("base-srv"),
{CRef, _} = base(Config),
{'CEA', ?caps{},
?packet{msg = ?answer_message{'Origin-Host' = OH,
'Result-Code' = 3010}}}
= client_recv(CRef).
base(Config) ->
connect(Config, base, []).
%% ====================
%% Have the base server reject a decent CER with the non-protocol
%% error DIAMETER_UNABLE_TO_COMPLY = 5012.
s_unable(Config) ->
server_reject(Config, fun base/1, 5012).
c_unable(Config) ->
client_closed(Config, "base-srv", fun base/1, 5012).
%% ====================
%% Have the client reject a decent CEA.
s_client_reject(Config) ->
true = diameter:subscribe(?SERVER),
OH = host(Config),
{_, LRef} = client_reject(Config),
receive
?event{service = ?SERVER,
info = {up, LRef,
{_, ?caps{origin_host = {_, OH}}},
{listen, _},
?packet{}}}
= Info ->
Info
after ?TIMEOUT ->
?fail({LRef, OH})
end.
c_client_reject(Config) ->
true = diameter:subscribe(?CLIENT),
OH = ?HOST("acct-srv"),
{CRef, _} = client_reject(Config),
{'CEA', {capabilities_cb, _, discard},
?caps{origin_host = {_, OH}},
?packet{msg = ?cea{'Result-Code' = 2001}}}
= client_recv(CRef).
client_reject(Config) ->
connect(Config, acct, [{capabilities_cb, fun client_capx/2}]).
%% ===========================================================================
%% server_closed/3
server_closed(Config, F, RC) ->
true = diameter:subscribe(?SERVER),
OH = host(Config),
{_, LRef} = F(Config),
receive
?event{service = ?SERVER,
info = {closed, LRef,
{'CER', RC,
?caps{origin_host = {_, OH}},
?packet{}}
= Reason,
{listen, _}}} ->
Reason
after ?TIMEOUT ->
?fail({LRef, OH})
end.
%% server_reject/3
server_reject(Config, F, RC) ->
true = diameter:subscribe(?SERVER),
OH = host(Config),
{_, LRef} = F(Config),
receive
?event{service = ?SERVER,
info = {closed, LRef,
{'CER', {capabilities_cb, _, RC},
?caps{origin_host = {_, OH}},
?packet{}}
= Reason,
{listen, _}}} ->
Reason
after ?TIMEOUT ->
?fail({LRef, OH})
end.
%% cliient_closed/4
client_closed(Config, Host, F, RC) ->
true = diameter:subscribe(?CLIENT),
OH = ?HOST(Host),
{CRef, _} = F(Config),
{'CEA', RC, ?caps{origin_host = {_, OH}}, ?packet{}}
= client_recv(CRef).
%% client_recv/1
client_recv(CRef) ->
receive
?event{service = ?CLIENT,
info = {closed, CRef, Reason, {connect, _}}} ->
Reason
after ?TIMEOUT ->
?fail(CRef)
end.
%% server_capx/3
server_capx(_, ?caps{origin_host = {_, [_,$_|"unknown_peer." ++ _]}}, _) ->
unknown;
server_capx(_, ?caps{origin_host = {_, [_,$_|"unable." ++ _]}}, _) ->
5012; %% DIAMETER_UNABLE_TO_COMPLY
server_capx(_, ?caps{origin_host = {OH,DH}}, _) ->
io:format("connection: ~p -> ~p~n", [DH,OH]),
ok.
%% client_capx/2
client_capx(_, ?caps{origin_host = {[_,$_|"client_reject." ++ _], _}}) ->
discard.
%% ===========================================================================
host(Config) ->
{_, H} = lists:keyfind(host, 1, Config),
?HOST(H).
listen(Name, Opts) ->
?util:listen(Name, tcp, Opts).
connect(Config, T, Opts) ->
{_, H} = lists:keyfind(host, 1, Config),
LRef = lref(Config, T),
CRef = connect(LRef, [{capabilities, [{'Origin-Host', ?HOST(H)}]}
| Opts]),
Name = lists:takewhile(fun(C) -> C /= $. end, H),
?util:write_priv(Config, Name, CRef), %% end_per_testcase reads
{CRef, LRef}.
connect(LRef, Opts) ->
[PortNr] = ?util:lport(tcp, LRef, 20),
{ok, CRef} = diameter:add_transport(?CLIENT,
{connect, opts(PortNr, Opts)}),
CRef.
opts(PortNr, Opts) ->
[{transport_module, diameter_tcp},
{transport_config, [{raddr, ?ADDR},
{rport, PortNr},
{ip, ?ADDR},
{port, 0}]}
| Opts].
lref(Config, T) ->
case ?util:read_priv(Config, ?MODULE) of
{LRef, _} when T == base ->
LRef;
{_, LRef} when T == acct ->
LRef
end.
%% ===========================================================================
%% diameter callbacks
peer_up(?SERVER,
{_, ?caps{origin_host = {"acct-srv." ++ _,
[_,$_|"client_reject." ++ _]}}},
State,
_) ->
State.
peer_down(?SERVER,
{_, ?caps{origin_host = {"acct-srv." ++ _,
[_,$_|"client_reject." ++ _]}}},
State,
_) ->
State.